From cbfad06e6fcfb66c8f6678dbb2e2b096df744726 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 06:12:54 +0900 Subject: [PATCH 001/184] Initial DOS VM tooling setup --- .gitignore | 31 ++ .pi/extensions/dos-vm.md | 20 ++ .pi/extensions/dos-vm.ts | 222 ++++++++++++++ .qemu/install.ps1 | 28 ++ .qemu/monitor.ps1 | 36 +++ .qemu/readme.txt | 144 +++++++++ .qemu/run.ps1 | 44 +++ .qemu/screenshot.ps1 | 26 ++ .qemu/send-keys.ps1 | 60 ++++ .qemu/serial-relay.mjs | 110 +++++++ .qemu/setup.ps1 | 26 ++ .qemu/share/dosagent.c | 240 +++++++++++++++ SPEC.md | 623 +++++++++++++++++++++++++++++++++++++++ TODO.md | 5 + 14 files changed, 1615 insertions(+) create mode 100644 .gitignore create mode 100644 .pi/extensions/dos-vm.md create mode 100644 .pi/extensions/dos-vm.ts create mode 100644 .qemu/install.ps1 create mode 100644 .qemu/monitor.ps1 create mode 100644 .qemu/readme.txt create mode 100644 .qemu/run.ps1 create mode 100644 .qemu/screenshot.ps1 create mode 100644 .qemu/send-keys.ps1 create mode 100644 .qemu/serial-relay.mjs create mode 100644 .qemu/setup.ps1 create mode 100644 .qemu/share/dosagent.c create mode 100644 SPEC.md create mode 100644 TODO.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4920e87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# QEMU runtime state, downloaded media, and generated captures +.qemu/* +!.qemu/*.ps1 +!.qemu/*.mjs +!.qemu/*.txt +!.qemu/share/ +!.qemu/share/*.c + +.qemu/*.qcow2 +.qemu/*.img +.qemu/*.iso +.qemu/*.zip +.qemu/*.png +.qemu/*.ppm +.qemu/*.tmp + +# Local logs and editor/OS metadata +*.log +*.tmp +*.swp +*~ +.DS_Store +Thumbs.db + +# Node/tool caches +node_modules/ +.npm/ +.cache/ + +# Temporary DOS build sources +.qemu/share/dosag*.c diff --git a/.pi/extensions/dos-vm.md b/.pi/extensions/dos-vm.md new file mode 100644 index 0000000..5893081 --- /dev/null +++ b/.pi/extensions/dos-vm.md @@ -0,0 +1,20 @@ +# DOS VM tools + +This project-local Pi extension exposes `dos_read`, `dos_write`, `dos_edit`, +`dos_list`, and `dos_exec`. It communicates with `DOSAGENT.EXE` over COM1 +through the serial relay controller port at `127.0.0.1:5555`. + +The agent is not started automatically. From the FreeDOS VGA console, run +`DOSAGENT` (installed in `C:\\FREEDOS\\BIN`, which is on `PATH`) when Pi tool +access is needed. While it runs, press Ctrl+C in the VGA console to stop it and +return to the DOS prompt; Pi tools then cannot connect until it is started +again. QEMU monitor control remains available at `127.0.0.1:4444`. + +Connect PuTTY in Raw mode to `127.0.0.1:5557` for a read-only decoded mirror of +Pi/DOS serial traffic. Port 5556 is reserved for the internal QEMU-to-relay +connection. + +`D:` is QEMU's FAT-directory view of `.qemu\\share`. Treat it as an exchange +volume, not a live-sync mount: host-side changes can be stale or partially +visible to a running VM. Restart QEMU before compiling host-edited files, or +use `dos_write` to place the source on `C:` before compiling. diff --git a/.pi/extensions/dos-vm.ts b/.pi/extensions/dos-vm.ts new file mode 100644 index 0000000..3721710 --- /dev/null +++ b/.pi/extensions/dos-vm.ts @@ -0,0 +1,222 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import net from "node:net"; + +const HOST = "127.0.0.1"; +const PORT = 5555; +const MONITOR_PORT = 4444; +const CHUNK_SIZE = 4096; +const MAX_FILE_SIZE = 4 * 1024 * 1024; + +function hexEncode(value: string | Buffer): string { + return Buffer.from(value).toString("hex").toUpperCase(); +} + +function hexDecode(value: string): Buffer { + return Buffer.from(value, "hex"); +} + +function normalizePath(path: string): string { + const result = path.startsWith("@") ? path.slice(1) : path; + if (!/^[A-Za-z]:[\\/]/.test(result)) { + throw new Error(`DOS path must be absolute (for example C:\\SRC\\FILE.C): ${result}`); + } + return result.replaceAll("/", "\\"); +} + +async function request(command: string, signal?: AbortSignal, timeoutMs = 30_000): Promise { + return new Promise((resolve, reject) => { + const socket = net.createConnection({ host: HOST, port: PORT }); + let buffered = ""; + let settled = false; + + const finish = (error?: Error, value?: string) => { + if (settled) return; + settled = true; + clearTimeout(timer); + signal?.removeEventListener("abort", abort); + socket.destroy(); + if (error) reject(error); else resolve(value ?? ""); + }; + const abort = () => finish(new Error("DOS tool call aborted")); + const timer = setTimeout(() => finish(new Error(`DOS agent timed out after ${timeoutMs}ms`)), timeoutMs); + + signal?.addEventListener("abort", abort, { once: true }); + socket.setEncoding("ascii"); + socket.on("connect", () => socket.write(`${command}\r\n`, "ascii")); + socket.on("data", (chunk) => { + buffered += chunk; + const lines = buffered.split(/\r?\n/); + buffered = lines.pop() ?? ""; + for (const line of lines) { + if (line.startsWith("OK ")) return finish(undefined, line.slice(3)); + if (line === "OK") return finish(undefined, ""); + if (line.startsWith("ERR ")) return finish(new Error(hexDecode(line.slice(4)).toString("utf8"))); + } + }); + socket.on("error", (error) => finish(new Error(`Cannot connect to DOS agent at ${HOST}:${PORT}: ${error.message}`))); + socket.on("close", () => { + if (!settled) finish(new Error("DOS serial connection closed before a response")); + }); + }); +} + +async function interruptForegroundCommand(): Promise { + await new Promise((resolve, reject) => { + const socket = net.createConnection({ host: HOST, port: MONITOR_PORT }); + const timer = setTimeout(() => { socket.destroy(); reject(new Error("QEMU monitor did not accept Ctrl+C")); }, 5_000); + socket.on("connect", () => socket.write("sendkey ctrl-c\r\n", "ascii")); + socket.on("data", () => { clearTimeout(timer); socket.destroy(); resolve(); }); + socket.on("error", (error) => { clearTimeout(timer); reject(new Error(`Cannot contact QEMU monitor: ${error.message}`)); }); + }); +} + +async function waitForAgentRecovery(timeoutMs = 10_000): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline) { + try { + if (await request("PING", undefined, 1_000) === "504F4E47") return true; + } catch { /* Command is still unwinding. */ } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + return false; +} + +async function readFile(path: string, signal?: AbortSignal): Promise { + const dosPath = normalizePath(path); + const chunks: Buffer[] = []; + let size = 0; + let offset = 0; + for (;;) { + const response = await request(`READ ${hexEncode(dosPath)} ${offset}`, signal); + const match = /^([01])(?:\s(.*))?$/.exec(response); + if (!match) throw new Error(`Malformed READ response: ${response}`); + const chunk = hexDecode(match[2] ?? ""); + chunks.push(chunk); + size += chunk.length; + if (size > MAX_FILE_SIZE) throw new Error(`DOS file exceeds ${MAX_FILE_SIZE} byte tool limit`); + offset += chunk.length; + if (match[1] === "1") return Buffer.concat(chunks, size); + if (chunk.length === 0) throw new Error("DOS agent returned an empty non-final chunk"); + } +} + +async function writeFile(path: string, content: Buffer, signal?: AbortSignal): Promise { + const dosPath = normalizePath(path); + if (content.length > MAX_FILE_SIZE) throw new Error(`Content exceeds ${MAX_FILE_SIZE} byte tool limit`); + if (content.length === 0) { + await request(`WRITE ${hexEncode(dosPath)} T `, signal); + return; + } + for (let offset = 0; offset < content.length; offset += CHUNK_SIZE) { + const chunk = content.subarray(offset, offset + CHUNK_SIZE); + await request(`WRITE ${hexEncode(dosPath)} ${offset === 0 ? "T" : "A"} ${hexEncode(chunk)}`, signal); + } +} + +function textResult(text: string) { + return { content: [{ type: "text" as const, text }], details: {} }; +} + +export default function (pi: ExtensionAPI) { + pi.registerTool({ + name: "dos_read", + label: "DOS Read", + description: "Read a text file from the running FreeDOS VM over COM1.", + promptSnippet: "Read files inside the running FreeDOS VM", + parameters: Type.Object({ path: Type.String({ description: "Absolute DOS path such as C:\\SRC\\MAIN.C" }) }), + async execute(_id, params, signal) { + const content = await readFile(params.path, signal); + return textResult(content.toString("utf8")); + }, + }); + + pi.registerTool({ + name: "dos_write", + label: "DOS Write", + description: "Create or replace a text file in the running FreeDOS VM over COM1.", + promptSnippet: "Write files inside the running FreeDOS VM", + parameters: Type.Object({ + path: Type.String({ description: "Absolute DOS path" }), + content: Type.String({ description: "Complete UTF-8 text content" }), + }), + async execute(_id, params, signal) { + const content = Buffer.from(params.content, "utf8"); + await writeFile(params.path, content, signal); + return textResult(`Wrote ${content.length} bytes to ${normalizePath(params.path)}`); + }, + }); + + pi.registerTool({ + name: "dos_edit", + label: "DOS Edit", + description: "Replace one uniquely matching text fragment in a file in the running FreeDOS VM.", + promptSnippet: "Make exact text replacements inside FreeDOS files", + parameters: Type.Object({ + path: Type.String({ description: "Absolute DOS path" }), + old_text: Type.String({ description: "Text that must occur exactly once" }), + new_text: Type.String({ description: "Replacement text" }), + }), + async execute(_id, params, signal) { + const original = (await readFile(params.path, signal)).toString("utf8"); + const occurrences = original.split(params.old_text).length - 1; + if (occurrences !== 1) throw new Error(`Expected old_text exactly once, found ${occurrences} occurrences`); + const updated = original.replace(params.old_text, params.new_text); + await writeFile(params.path, Buffer.from(updated, "utf8"), signal); + return textResult(`Edited ${normalizePath(params.path)}`); + }, + }); + + pi.registerTool({ + name: "dos_list", + label: "DOS List", + description: "List a directory in the running FreeDOS VM over COM1.", + promptSnippet: "List directories inside the running FreeDOS VM", + parameters: Type.Object({ path: Type.String({ description: "Absolute DOS directory path" }) }), + async execute(_id, params, signal) { + const response = await request(`LIST ${hexEncode(normalizePath(params.path))}`, signal); + return textResult(hexDecode(response).toString("utf8")); + }, + }); + + pi.registerTool({ + name: "dos_exec", + label: "DOS Exec", + description: "Execute a command in the running FreeDOS VM and return its output.", + promptSnippet: "Run commands inside the running FreeDOS VM", + parameters: Type.Object({ + command: Type.String({ description: "FreeDOS command line" }), + timeout_seconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 120, description: "Abort after this many seconds (default: 30)" })), + }), + async execute(_id, params, signal) { + if (params.command.length > 600) throw new Error("DOS command exceeds 600 characters"); + const timeoutMs = (params.timeout_seconds ?? 30) * 1_000; + let response: string; + try { + response = await request(`EXEC ${hexEncode(params.command)}`, signal, timeoutMs); + } catch (error) { + if (signal?.aborted) throw error; + await interruptForegroundCommand(); + const recovered = await waitForAgentRecovery(); + throw new Error(`DOS command timed out after ${timeoutMs / 1_000}s and was interrupted with Ctrl+C${recovered ? "; agent recovered" : "; agent did not recover"}`); + } + const match = /^(-?\d+)(?:\s(.*))?$/.exec(response); + if (!match) throw new Error(`Malformed EXEC response: ${response}`); + const output = hexDecode(match[2] ?? "").toString("utf8"); + return textResult(`${output}${output.endsWith("\n") || !output ? "" : "\n"}[exit ${match[1]}]`); + }, + }); + + pi.registerTool({ + name: "dos_abort", + label: "DOS Abort", + description: "Send Ctrl+C to interrupt the foreground DOS command without rebooting the VM.", + promptSnippet: "Interrupt a hung DOS command without rebooting the VM", + parameters: Type.Object({}), + async execute() { + await interruptForegroundCommand(); + const recovered = await waitForAgentRecovery(); + return textResult(recovered ? "Sent Ctrl+C; DOS agent recovered." : "Sent Ctrl+C; DOS agent has not responded yet."); + }, + }); +} diff --git a/.qemu/install.ps1 b/.qemu/install.ps1 new file mode 100644 index 0000000..2d3ade9 --- /dev/null +++ b/.qemu/install.ps1 @@ -0,0 +1,28 @@ +param() + +$ErrorActionPreference = 'Stop' +$qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop +$disk = Join-Path $PSScriptRoot 'freedos.qcow2' +$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' + +if (-not (Test-Path -LiteralPath $disk)) { + throw "Missing $disk. Create it first with: qemu-img create -f qcow2 .qemu\\freedos.qcow2 2G" +} +if (-not (Test-Path -LiteralPath $iso)) { + throw "Missing $iso. Run .qemu\\setup.ps1 first." +} +if (Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue) { + throw 'QEMU monitor port 4444 is already in use. Stop the existing VM first.' +} + +& $qemu.Source ` + -machine pc,usb=on ` + -cpu pentium3 ` + -m 64 ` + -drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" ` + -drive "file=$iso,media=cdrom,readonly=on" ` + -nic user,model=ne2k_isa ` + -monitor "tcp:127.0.0.1:4444,server=on,wait=off" ` + -serial null ` + -boot order=d ` + -display default diff --git a/.qemu/monitor.ps1 b/.qemu/monitor.ps1 new file mode 100644 index 0000000..6a0322f --- /dev/null +++ b/.qemu/monitor.ps1 @@ -0,0 +1,36 @@ +param( + [Parameter(Mandatory = $true, Position = 0)] + [string]$Command +) + +$ErrorActionPreference = 'Stop' +$client = [System.Net.Sockets.TcpClient]::new() + +try { + $client.Connect('127.0.0.1', 4444) + $stream = $client.GetStream() + $stream.ReadTimeout = 1000 + $writer = [System.IO.StreamWriter]::new($stream, [System.Text.Encoding]::ASCII, 1024, $true) + $writer.NewLine = "`n" + $writer.AutoFlush = $true + + Start-Sleep -Milliseconds 100 + while ($stream.DataAvailable) { + $buffer = New-Object byte[] 4096 + [void]$stream.Read($buffer, 0, $buffer.Length) + } + + $writer.WriteLine($Command) + Start-Sleep -Milliseconds 200 + + $result = New-Object System.Text.StringBuilder + while ($stream.DataAvailable) { + $buffer = New-Object byte[] 4096 + $count = $stream.Read($buffer, 0, $buffer.Length) + [void]$result.Append([System.Text.Encoding]::ASCII.GetString($buffer, 0, $count)) + } + $result.ToString().Trim() +} +finally { + $client.Dispose() +} diff --git a/.qemu/readme.txt b/.qemu/readme.txt new file mode 100644 index 0000000..bd4facd --- /dev/null +++ b/.qemu/readme.txt @@ -0,0 +1,144 @@ +############################################################################### + FreeDOS 1.4 ("FreeDOS 1.4") +############################################################################### + + +------------------------------------------------------------------------------- + General system requirements: +------------------------------------------------------------------------------- + + * DOS-compatible system (Intel + BIOS, or UEFI with Legacy support) + + * At least 20MB free disk space: + + 20MB Plain DOS system + 30MB Plain DOS system, with sources + + 275MB Full installation including applications and games + 450MB Full installation with sources + + +------------------------------------------------------------------------------- + What's in all those zip files? +------------------------------------------------------------------------------- + +FD14-LiveCD.zip + + * FD14BOOT.IMG - Basic FreeDOS installation boot floppy image. + If your computer has a CD-ROM drive, but you cannot boot from the Live CD + or Legacy CD. Use this diskette image to boot the system. Then insert the + install CD. The FreeDOS installer should do the rest. This diskette + image is for installation purposes only and does not provide a Live + Environment. + + * FD14LIVE.ISO - The FreeDOS 1.4 installer. Most users should + use this image to install FreeDOS. + + Depending on your computer system and hardware configuration, you + can also use the LiveCD to boot and run FreeDOS directly from the + CD-ROM without installation to your hard drive. + +FD14-LegacyCD.zip + + * FD14BOOT.IMG - This zip archive also contains a copy of the basic + CD-ROM installation boot floppy. + + * FD14LGCY.ISO - A bootable CD image designed for older hardware. If + you cannot boot the LiveCD to install FreeDOS, try this disc image. + + This disc image uses the older El Torito boot CD format. Some newer + computers and virtual machines cannot use this older format. Unless + you have a computer that requires this type of bootable CD, we + recommend using the LiveCD instead. + +FD14-BonusCD.zip + + * FD14BNS.ISO - A non-bootable CD image that contains some FreeDOS + packages that are not installed as part of either the LiveCD or + the Legacy CD. + +FD14-LiteUSB.zip + + * FD14LITE.IMG - A minimal FreeDOS installer, as a USB fob drive + image. This does not contain all of the packages from either the + LiveCD or the LegacyCD, and instead only contains a basic set of + FreeDOS packages. + + * FD14LITE.VMDK - A virtual machine disk file, compatible with a + variety of virtual machine software including VirtualBox, VMware, + and other systems. + + Using a VMDK file can simplify installing FreeDOS. Just attach the + VMDK image to your virtual machine software as a hard drive, and + boot it. (Please note that you will still need to create a virtual + hard disk to install FreeDOS) + +FD14-FullUSB.zip + + * FD14FULL.IMG - Plain DOS system and Full install USB stick image. + + * FD14FULL.VMDK - A virtual machine disk file, compatible with a + variety of virtual machine software. Just attach the VMDK image to + your virtual machine as a hard drive, and boot it. + +VERIFY.TXT + + * Contains MD5, SHA256 and SHA512 hashes for all of the different + release files. You can verify your copy of FreeDOS with these. + +README.TXT + + * The "before you choose and install" document. (All of the zip + files listed above also have a copy of the README file.) + + +------------------------------------------------------------------------------- + FreeDOS Floppy-Only Edition (FD14-x86) +------------------------------------------------------------------------------- + +FreeDOS 1.4 includes a Floppy-Only Edition! This edition should run on +any hardware that can run FreeDOS and has EGA or better graphics: + + * Are you running a '286 or another classic system without a CD-ROM + drive? Install from these floppies to install FreeDOS. + + * Do you have just one hard drive and no CD or floppy drive? Just + copy the contents of the floppies to a temporary directory and run + the installer from there. + + * Want to perform a "headless" install to a different DOS directory? + It's easy with the command line options. + +The Floppy-Only Edition uses a completely different installer than +the CD-ROM or USB installers. The Floppy-Only Edition does not use +any of those other media to install. + +The Floppy-Only Edition contains a limited set of FreeDOS programs +that are more useful on classic PC hardware. + +The FreeDOS Floppy-Only Edition is distributed as single zip archive that +contains several pre-made floppy diskette images: + + These zip archives contain image files for several common floppy + diskette media under separate directories: + + * 720k - 3.5" 720k diskette images + + * 144m - 3.5" 1.44mb diskette images + + * 120m - 5.25" 1.2mb diskette images + + Each of those sets contain a number of pre-made disk images: + + * x86BOOT.img - A floppy boot disk image with the x86 installer. + + * x86DSK??.img - Several floppy diskette images that contain the + core FreeDOS operating system files. The number of floppy images + and amount of files on each varies depending on the diskette + capacity. + +To conserve space, the FreeDOS Floppy-Only Edition does not contain +the source code for the FreeDOS packages. You can find the source code +via the FreeDOS website (https://www.freedos.org/download/) or from +the other release media, like the USB or CD-ROM installer. + diff --git a/.qemu/run.ps1 b/.qemu/run.ps1 new file mode 100644 index 0000000..865e496 --- /dev/null +++ b/.qemu/run.ps1 @@ -0,0 +1,44 @@ +param() + +$ErrorActionPreference = 'Stop' +$qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop +$node = Get-Command node.exe -ErrorAction Stop +$disk = Join-Path $PSScriptRoot 'freedos.qcow2' +$share = Join-Path $PSScriptRoot 'share' +$relay = Join-Path $PSScriptRoot 'serial-relay.mjs' + +# QEMU's FAT-directory drive is a convenient exchange volume, but it is not a +# reliable live-sync mechanism. Restart QEMU after host-side source changes. +New-Item -ItemType Directory -Force -Path $share | Out-Null + +if (-not (Test-Path -LiteralPath $disk)) { + throw "Missing $disk. Run .qemu\\setup.ps1, then .qemu\\install.ps1." +} + +$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5556 -ErrorAction SilentlyContinue +if (-not $relayListening) { + Start-Process -FilePath $node.Source -ArgumentList @($relay) -WorkingDirectory $PSScriptRoot -WindowStyle Hidden + $deadline = [DateTime]::UtcNow.AddSeconds(5) + do { + Start-Sleep -Milliseconds 100 + $relayListening = Get-NetTCPConnection -State Listen -LocalPort 5556 -ErrorAction SilentlyContinue + } while (-not $relayListening -and [DateTime]::UtcNow -lt $deadline) + if (-not $relayListening) { throw 'Serial relay did not start.' } +} + +$monitorListening = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue +if ($monitorListening) { + throw 'QEMU monitor port 4444 is already in use. Stop the existing VM before starting another one.' +} + +& $qemu.Source ` + -machine pc,accel=whpx,kernel-irqchip=off,usb=on ` + -smp 1 ` + -m 64 ` + -drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" ` + -hdb "fat:rw:$share" ` + -nic user,model=ne2k_isa ` + -monitor "tcp:127.0.0.1:4444,server=on,wait=off" ` + -serial "tcp:127.0.0.1:5556" ` + -boot order=c ` + -display default diff --git a/.qemu/screenshot.ps1 b/.qemu/screenshot.ps1 new file mode 100644 index 0000000..bd5c93d --- /dev/null +++ b/.qemu/screenshot.ps1 @@ -0,0 +1,26 @@ +param( + [string]$Name = 'qemu-screen', + [switch]$KeepPpm +) + +$ErrorActionPreference = 'Stop' +$ffmpeg = Get-Command ffmpeg.exe -ErrorAction Stop + +$target = if ([IO.Path]::IsPathRooted($Name)) { $Name } else { Join-Path $PSScriptRoot $Name } +$extension = [IO.Path]::GetExtension($target) +if ($extension -in @('.png', '.ppm')) { + $target = [IO.Path]::Combine([IO.Path]::GetDirectoryName($target), [IO.Path]::GetFileNameWithoutExtension($target)) +} +$directory = [IO.Path]::GetDirectoryName($target) +if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null } + +$ppm = "$target.ppm" +$png = "$target.png" +$qemuPath = $ppm.Replace('\', '/') + +& (Join-Path $PSScriptRoot 'monitor.ps1') "screendump $qemuPath" | Out-Null +if (-not (Test-Path -LiteralPath $ppm)) { throw 'QEMU did not create the requested screenshot.' } +& $ffmpeg.Source -y -loglevel error -i $ppm $png +if ($LASTEXITCODE -ne 0) { throw "ffmpeg conversion failed with exit code $LASTEXITCODE." } +if (-not $KeepPpm) { Remove-Item -LiteralPath $ppm -Force } +Get-Item -LiteralPath $png diff --git a/.qemu/send-keys.ps1 b/.qemu/send-keys.ps1 new file mode 100644 index 0000000..3431978 --- /dev/null +++ b/.qemu/send-keys.ps1 @@ -0,0 +1,60 @@ +param( + [Parameter(Mandatory)] + [string] $Text, + + [ValidateRange(0, 1000)] + [int] $DelayMilliseconds = 25, + + [switch] $NoEnter +) + +$ErrorActionPreference = 'Stop' + +$map = @{ + ' ' = 'spc'; ':' = 'shift-semicolon'; ';' = 'semicolon' + '\' = 'backslash'; '|' = 'shift-backslash'; '/' = 'slash'; '?' = 'shift-slash' + '.' = 'dot'; '>' = 'shift-dot'; ',' = 'comma'; '<' = 'shift-comma' + '-' = 'minus'; '_' = 'shift-minus'; '=' = 'equal'; '+' = 'shift-equal' + '[' = 'bracket_left'; '{' = 'shift-bracket_left' + ']' = 'bracket_right'; '}' = 'shift-bracket_right' + "'" = 'apostrophe'; '"' = 'shift-apostrophe'; '`' = 'grave_accent'; '~' = 'shift-grave_accent' + '!' = 'shift-1'; '@' = 'shift-2'; '#' = 'shift-3'; '$' = 'shift-4'; '%' = 'shift-5' + '^' = 'shift-6'; '&' = 'shift-7'; '*' = 'shift-8'; '(' = 'shift-9'; ')' = 'shift-0' +} + +function ConvertTo-QemuKey([char] $Character) { + $text = [string] $Character + if ($map.ContainsKey($text)) { return $map[$text] } + if ([char]::IsLetter($Character)) { + $letter = [char]::ToLowerInvariant($Character) + if ([char]::IsUpper($Character)) { return "shift-$letter" } + return [string] $letter + } + if ([char]::IsDigit($Character)) { return $text } + throw "Unsupported QEMU key character: '$Character'" +} + +$client = [System.Net.Sockets.TcpClient]::new([System.Net.Sockets.AddressFamily]::InterNetwork) +try { + $client.Connect([System.Net.IPAddress]::Parse('127.0.0.1'), 4444) + $stream = $client.GetStream() + $writer = [System.IO.StreamWriter]::new($stream, [System.Text.Encoding]::ASCII, 1024, $true) + try { + $writer.NewLine = "`r`n" + $writer.AutoFlush = $true + Start-Sleep -Milliseconds 100 + + foreach ($char in $Text.ToCharArray()) { + $writer.WriteLine("sendkey $(ConvertTo-QemuKey $char)") + Start-Sleep -Milliseconds $DelayMilliseconds + } + if (-not $NoEnter) { $writer.WriteLine('sendkey ret') } + Start-Sleep -Milliseconds $DelayMilliseconds + } + finally { + $writer.Dispose() + } +} +finally { + $client.Dispose() +} diff --git a/.qemu/serial-relay.mjs b/.qemu/serial-relay.mjs new file mode 100644 index 0000000..de44160 --- /dev/null +++ b/.qemu/serial-relay.mjs @@ -0,0 +1,110 @@ +import net from "node:net"; + +const HOST = "127.0.0.1"; +const QEMU_PORT = 5556; +const TOOL_PORT = 5555; +const OBSERVER_PORT = 5557; + +let qemu = null; +let controller = null; +const observers = new Set(); + +function safeWrite(socket, data) { + if (socket && !socket.destroyed && socket.writable) socket.write(data); +} + +function decodeHex(hex) { + if (!hex || hex.length % 2 || !/^[0-9a-f]+$/i.test(hex)) return null; + return Buffer.from(hex, "hex").toString("utf8"); +} + +function visible(text) { + const shortened = text.length > 400 ? `${text.slice(0, 400)}…` : text; + return shortened + .replace(/\r\n?/g, "\n") + .replace(/[^\x20-\x7E\n]/g, (char) => `[0x${char.charCodeAt(0).toString(16).padStart(2, "0")}]`) + .replace(/\n/g, "\r\n "); +} + +function formatLine(source, line) { + if (!line) return null; + if (line === "PING" || line === "QUIT" || line === "DOSAGENT READY") return `${source} ${line}`; + + const [command, ...args] = line.split(" "); + if (["READ", "LIST"].includes(command) && args[0]) { + return `${source} ${command} ${visible(decodeHex(args[0]) ?? args[0])}${args[1] ? ` @${args[1]}` : ""}`; + } + if (command === "WRITE" && args.length >= 3) { + const path = decodeHex(args[0]) ?? args[0]; + const body = decodeHex(args.slice(2).join(" ")) ?? args.slice(2).join(" "); + return `${source} WRITE ${visible(path)} (${args[1] === "T" ? "replace" : "append"}) ${visible(body)}`; + } + if (command === "EXEC" && args[0]) return `${source} EXEC ${visible(decodeHex(args.join(" ")) ?? args.join(" "))}`; + if (command === "ERR" && args[0]) return `${source} ERROR ${visible(decodeHex(args.join(" ")) ?? args.join(" "))}`; + if (command === "OK") { + const singlePayload = args.length === 1 ? decodeHex(args[0]) : null; + const payload = singlePayload ?? (args.length > 1 ? decodeHex(args.slice(1).join(" ")) : null); + const status = singlePayload !== null ? "" : (args[0] ? ` (${args[0]})` : ""); + return `${source} OK${status}${payload !== null ? ` ${visible(payload)}` : ""}`; + } + return `${source} ${line}`; +} + +const traceBuffers = new Map(); +function trace(source, data) { + const pending = (traceBuffers.get(source) ?? "") + data.toString("ascii"); + const parts = pending.split(/\r?\n/); + traceBuffers.set(source, parts.pop()); + for (const line of parts) { + const formatted = formatLine(source, line); + if (formatted) for (const observer of observers) safeWrite(observer, `${formatted}\r\n`); + } +} + +const qemuServer = net.createServer((socket) => { + if (qemu && !qemu.destroyed) { + socket.end(); + return; + } + qemu = socket; + socket.setNoDelay(true); + socket.on("data", (data) => { + safeWrite(controller, data); + trace("DOS -> Pi", data); + }); + socket.on("close", () => { + if (qemu === socket) qemu = null; + if (controller && !controller.destroyed) controller.destroy(); + }); + socket.on("error", () => {}); +}); + +const toolServer = net.createServer((socket) => { + if (controller && !controller.destroyed) { + socket.end("ERR 53455249414C20434F4E54524F4C4C45522042555359\r\n"); + return; + } + controller = socket; + socket.setNoDelay(true); + socket.on("data", (data) => { + safeWrite(qemu, data); + trace("Pi -> DOS", data); + }); + socket.on("close", () => { + if (controller === socket) controller = null; + }); + socket.on("error", () => {}); +}); + +const observerServer = net.createServer((socket) => { + observers.add(socket); + socket.setNoDelay(true); + socket.write("DOS serial observer - decoded protocol (read-only)\r\n"); + socket.on("data", () => {}); + socket.on("close", () => observers.delete(socket)); + socket.on("error", () => observers.delete(socket)); +}); + +qemuServer.listen(QEMU_PORT, HOST); +toolServer.listen(TOOL_PORT, HOST); +observerServer.listen(OBSERVER_PORT, HOST); diff --git a/.qemu/setup.ps1 b/.qemu/setup.ps1 new file mode 100644 index 0000000..58b944e --- /dev/null +++ b/.qemu/setup.ps1 @@ -0,0 +1,26 @@ +param() + +$ErrorActionPreference = 'Stop' +$qemuImg = Get-Command qemu-img.exe -ErrorAction Stop +$disk = Join-Path $PSScriptRoot 'freedos.qcow2' +$archive = Join-Path $PSScriptRoot 'FD14-LiveCD.zip' +$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' +$url = 'https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/distributions/1.4/FD14-LiveCD.zip' + +if (-not (Test-Path -LiteralPath $disk)) { + & $qemuImg.Source create -f qcow2 $disk 2G + if ($LASTEXITCODE -ne 0) { throw "qemu-img failed with exit code $LASTEXITCODE." } +} + +if (-not (Test-Path -LiteralPath $iso)) { + if (-not (Test-Path -LiteralPath $archive)) { + Invoke-WebRequest -Uri $url -OutFile $archive + } + Expand-Archive -LiteralPath $archive -DestinationPath $PSScriptRoot -Force +} + +if (-not (Test-Path -LiteralPath $iso)) { + throw 'FreeDOS ISO extraction failed.' +} + +Get-Item -LiteralPath $disk,$iso | Select-Object Name,Length,LastWriteTime diff --git a/.qemu/share/dosagent.c b/.qemu/share/dosagent.c new file mode 100644 index 0000000..01095b4 --- /dev/null +++ b/.qemu/share/dosagent.c @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include +#include + +#define LINE_SIZE 9000 +#define CHUNK_SIZE 4096 + +static char linebuf[LINE_SIZE]; +static unsigned char data[CHUNK_SIZE + 1]; + +#define COM1_BASE 0x3F8 + +static void serial_init(void) { + outp(COM1_BASE + 1, 0x00); + outp(COM1_BASE + 3, 0x80); + outp(COM1_BASE + 0, 12); + outp(COM1_BASE + 1, 0); + outp(COM1_BASE + 3, 0x03); + outp(COM1_BASE + 2, 0xC7); + outp(COM1_BASE + 4, 0x0B); +} + +static int console_break_requested(void) { + unsigned short key = _bios_keybrd(_KEYBRD_READY); + if ((key & 0xFF) != 3) return 0; + _bios_keybrd(_KEYBRD_READ); + return 1; +} + +static int serial_getc(void) { + while ((inp(COM1_BASE + 5) & 0x01) == 0) { + if (console_break_requested()) return -1; + } + return inp(COM1_BASE); +} + +static void serial_putc(int c) { + while ((inp(COM1_BASE + 5) & 0x20) == 0) { } + outp(COM1_BASE, c); +} + +static void serial_write(const char *text) { + while (*text) serial_putc((unsigned char)*text++); +} + +static int serial_readline(char *buffer, int cap) { + int used = 0; + int c; + for (;;) { + c = serial_getc(); + if (c < 0) return -1; + if (c == '\r' || c == '\n') { + if (used == 0) continue; + buffer[used] = '\0'; + return used; + } + if (used < cap - 1) buffer[used++] = (char)c; + } +} + +static int hexval(int c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +static int decode_hex(const char *src, unsigned char *dst, int cap) { + int n = 0; + int hi, lo; + while (*src && src[1]) { + if (n >= cap) return -1; + hi = hexval((unsigned char)src[0]); + lo = hexval((unsigned char)src[1]); + if (hi < 0 || lo < 0) return -1; + dst[n++] = (unsigned char)((hi << 4) | lo); + src += 2; + } + return *src ? -1 : n; +} + +static void print_hex(const unsigned char *src, unsigned count) { + static const char digits[] = "0123456789ABCDEF"; + unsigned i; + for (i = 0; i < count; ++i) { + serial_putc(digits[src[i] >> 4]); + serial_putc(digits[src[i] & 15]); + } +} + +static void ok_data(const unsigned char *src, unsigned count) { + serial_write("OK "); + print_hex(src, count); + serial_write("\r\n"); +} + +static void error_text(const char *message) { + serial_write("ERR "); + print_hex((const unsigned char *)message, strlen(message)); + serial_write("\r\n"); +} + +static int decode_path(const char *hex, char *path, int cap) { + int n = decode_hex(hex, (unsigned char *)path, cap - 1); + if (n < 0) return 0; + path[n] = '\0'; + return 1; +} + +static void command_read(char *args) { + char path[260]; + char *offset_text = strchr(args, ' '); + FILE *file; + long offset; + size_t count; + int eof; + + if (!offset_text) { error_text("READ requires path and offset"); return; } + *offset_text++ = '\0'; + if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } + offset = atol(offset_text); + file = fopen(path, "rb"); + if (!file) { error_text("Cannot open file"); return; } + if (fseek(file, offset, SEEK_SET) != 0) { fclose(file); error_text("Cannot seek file"); return; } + count = fread(data, 1, CHUNK_SIZE, file); + eof = feof(file) ? 1 : 0; + if (count < CHUNK_SIZE) eof = 1; + fclose(file); + sprintf(linebuf, "OK %d ", eof); + serial_write(linebuf); + print_hex(data, count); + serial_write("\r\n"); +} + +static void command_write(char *args) { + char path[260]; + char *mode_text = strchr(args, ' '); + char *payload; + FILE *file; + int count; + + if (!mode_text) { error_text("WRITE requires path, mode and data"); return; } + *mode_text++ = '\0'; + payload = strchr(mode_text, ' '); + if (!payload) { error_text("WRITE requires data"); return; } + *payload++ = '\0'; + if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } + count = decode_hex(payload, data, CHUNK_SIZE); + if (count < 0) { error_text("Invalid data encoding"); return; } + file = fopen(path, mode_text[0] == 'A' ? "ab" : "wb"); + if (!file) { error_text("Cannot write file"); return; } + if (count && fwrite(data, 1, count, file) != (size_t)count) { + fclose(file); error_text("Short write"); return; + } + fclose(file); + ok_data((const unsigned char *)"", 0); +} + +static void command_list(char *args) { + char path[260]; + char pattern[300]; + char output[CHUNK_SIZE]; + struct find_t found; + unsigned used = 0; + int rc; + + if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } + strcpy(pattern, path); + if (pattern[0] && pattern[strlen(pattern) - 1] != '\\' && pattern[strlen(pattern) - 1] != '/') strcat(pattern, "\\"); + strcat(pattern, "*.*"); + rc = _dos_findfirst(pattern, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_SUBDIR | _A_ARCH, &found); + while (rc == 0) { + char entry[100]; + int len; + if (strcmp(found.name, ".") && strcmp(found.name, "..")) { + sprintf(entry, "%s\t%lu\t%s\n", found.name, found.size, (found.attrib & _A_SUBDIR) ? "DIR" : "FILE"); + len = strlen(entry); + if (used + len >= sizeof(output)) break; + memcpy(output + used, entry, len); + used += len; + } + rc = _dos_findnext(&found); + } + ok_data((const unsigned char *)output, used); +} + +static void command_exec(char *args) { + char command[700]; + char shell_command[800]; + const char *temp_path = "C:\\PIEXEC.TMP"; + FILE *file; + int n, exit_code; + size_t count; + + n = decode_hex(args, (unsigned char *)command, sizeof(command) - 1); + if (n < 0) { error_text("Invalid command encoding"); return; } + command[n] = '\0'; + sprintf(shell_command, "COMMAND.COM /C %s > %s", command, temp_path); + exit_code = system(shell_command); + file = fopen(temp_path, "rb"); + count = file ? fread(data, 1, CHUNK_SIZE, file) : 0; + if (file) fclose(file); + remove(temp_path); + sprintf(linebuf, "OK %d ", exit_code); + serial_write(linebuf); + print_hex(data, count); + serial_write("\r\n"); +} + +int main(void) { + char *command; + char *args; + + serial_init(); + cprintf("\r\n[DOSAGENT] Ready on COM1. Press Ctrl+C here to stop.\r\n"); + serial_write("DOSAGENT READY\r\n"); + for (;;) { + if (serial_readline(linebuf, sizeof(linebuf)) < 0) { + cprintf("\r\n[DOSAGENT] Stopped from VGA console.\r\n"); + break; + } + command = linebuf; + args = strchr(command, ' '); + if (args) *args++ = '\0'; else args = command + strlen(command); + + cprintf("[DOSAGENT] %s\r\n", command); + + if (!strcmp(command, "PING")) ok_data((const unsigned char *)"PONG", 4); + else if (!strcmp(command, "READ")) command_read(args); + else if (!strcmp(command, "WRITE")) command_write(args); + else if (!strcmp(command, "LIST")) command_list(args); + else if (!strcmp(command, "EXEC")) command_exec(args); + else if (!strcmp(command, "QUIT")) { ok_data((const unsigned char *)"BYE", 3); break; } + else error_text("Unknown command"); + } + return 0; +} diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..746cc2c --- /dev/null +++ b/SPEC.md @@ -0,0 +1,623 @@ +# Ferro 언어 명세 v0.1 + +DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. +파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. + +이 문서는 언어 명세 + 컴파일러 구현 지시서를 겸한다. 구현 중 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. + +--- + +## 1. 설계 철학 + +1. **안전은 기본, 위험은 명시.** 기본 코드는 메모리 안전(널 역참조, 버퍼 오버런, use-after-free, 이중 해제 불가). 위험한 연산은 `unsafe {}` 블록 안에서만. +2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 640KB 머신에서 셀프호스팅을 가능하게 한다. +3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음. +4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능. +5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §13에 기록한다. +6. **기존 도구체인 재사용.** 링커, `.OBJ`/`.LIB`/`.EXE` 포맷, DPMI 익스텐더를 새로 만들지 않는다. + +--- + +## 2. 타깃 + +| | `bits16` | `bits32` | +|---|---|---| +| CPU/모드 | 8086 리얼모드 | 386 보호모드 플랫 (DPMI) | +| `usize`/`isize` | 16비트 | 32비트 | +| 포인터 크기 | near 2B / far 4B | 4B (far 없음) | +| 메모리 모델 | small, large | flat | +| 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | + +- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large]` +- 소스 분기: `comptime if @bits == 16 { ... } else { ... }` +- `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. +- 표준 라이브러리는 코어 공용, `sys` 유닛만 타깃별 구현. + +--- + +## 3. 어휘 구조 + +- 식별자: `[A-Za-z_][A-Za-z0-9_]*`. 대소문자 구분. +- 주석: `//` 줄 끝까지, `/* */` **중첩 허용**. +- 정수 리터럴: `123`, `0xFF`, `0b1010`, `0o17`, 자릿수 구분 `1_000_000`. +- 문자 리터럴: `'a'`, `'\n'`, `'\x41'` → 타입 `char`. +- 문자열 리터럴: `"abc"` → 타입 `str`. NUL 종료 아님. 이스케이프는 문자 리터럴과 동일. 인접 리터럴 자동 연결 없음. +- 불린: `true`, `false`. 옵셔널 널: `null`. +- 세미콜론 필수. 블록 중괄호 필수(단문 `if`도 `{}` 필요). + +**예약어:** +``` +unit import pub fn struct enum error const static var let mut +if else while for in match return break continue defer +unsafe comptime asm try catch as extern interrupt far +true false null self Self type +``` + +--- + +## 4. 타입 시스템 + +### 4.1 기본 타입 + +- 정수: `i8 i16 i32 u8 u16 u32 usize isize` +- `bool` (1바이트, 정수와 상호 변환 없음) +- `char` (u8과 크기 같지만 별개 타입) +- `void` (반환 타입으로만) +- `type` (comptime 파라미터에서만, §9) + +**정수 규칙:** +- 서로 다른 정수 타입 간 암묵 변환 없음. `as`로 명시. +- `as`는 절단/부호확장을 수행하며 값 손실을 검사하지 않는다. +- `+ - * / %`는 검사 빌드에서 오버플로 시 트랩. `+% -% *%`는 랩어라운드(항상 무검사). +- `/`, `%`의 0 나눗셈은 항상 트랩(검사 빌드 여부 무관, CPU가 트랩함). +- 시프트 `<< >>`: 우변은 `u8`. 시프트 양이 비트폭 이상이면 검사 빌드에서 트랩. +- 비트 연산 `& | ^ ~`는 같은 타입끼리만. + +### 4.2 복합 타입 + +| 문법 | 의미 | 표현 | +|---|---|---| +| `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` | +| `[]T` | 슬라이스 (참조성, §5 R4 적용) | `(ptr, len)` | +| `str` | `[]u8` 불변 별칭 | `(ptr, len)` | +| `^T` | 소유 포인터 (힙, 단일 소유자) | 포인터 | +| `&T` | 공유 참조 | 포인터 | +| `&mut T` | 배타 참조 | 포인터 | +| `*T` | raw 포인터 (`unsafe`에서만 역참조) | 포인터 | +| `far ^T`, `far *T`, `far &T` | far 포인터 (`bits16` 전용) | 4바이트 | +| `?T` | 옵셔널 | 널 표현 가능 타입은 크기 동일, 아니면 `(bool, T)` | +| `E!T` / `!T` | 에러 유니온 (`!T`는 기본 에러 집합) | `(u16 err, T val)` | +| `fn(A, B) -> R` | 함수 포인터 | 포인터 | + +- **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. +- 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. +- 소유 슬라이스가 필요하면 `^[]T`(길이 있는 힙 버퍼)를 쓴다. `mem.alloc_slice(T, n)`가 반환. + +### 4.3 구조체 + +```fe +pub struct Point { + x: i32, + y: i32, + + pub fn new(x: i32, y: i32) -> Point { return Point{ x: x, y: y }; } + pub fn len2(self: &Self) -> i32 { return self.x*self.x + self.y*self.y; } + pub fn shift(self: &mut Self, dx: i32) { self.x += dx; } +} +``` + +- 리터럴: `Point{ x: 1, y: 2 }`. 모든 필드 명시 필수(기본값 없음). +- 메서드는 struct 블록 안에 정의. 첫 파라미터가 `self: Self | &Self | &mut Self`면 메서드. +- `x.f(y)`는 `Point.f(x, y)`의 설탕. 자동 참조 취함(`x.shift(1)`은 `Point.shift(&mut x, 1)`). +- `Self`는 자기 타입의 별칭. +- 필드 레이아웃은 선언 순서. 정렬은 타깃 규칙(`bits16`은 1바이트 정렬, `bits32`는 자연 정렬). `packed struct`로 정렬 강제 해제. +- 소멸자: `fn drop(self: &mut Self)`를 정의하면 스코프 종료 시 자동 호출(§5 R3). + +### 4.4 열거형 (태그드 유니온) + +```fe +pub enum Shape { + Empty, + Circle(i32), + Rect{ w: i32, h: i32 }, +} +``` + +- 표현: `struct { u8 tag; union {...} payload; }`. 배리언트 256개 초과 시 `u16` 태그. +- 페이로드 없는 배리언트만 있는 열거형은 정수처럼 취급되며 `as u8` 가능. +- 생성: `Shape.Circle(5)`, `Shape.Rect{ w: 3, h: 4 }`, `Shape.Empty`. +- 해체는 `match` 또는 `if let`으로만. 직접 필드 접근 불가. + +### 4.5 옵셔널 + +```fe +var p: ?^Node = null; +if let Some(node) = p { node.value = 1; } // node: &mut Node (p가 mut일 때) +let v = p.?; // null이면 트랩 +let v = p orelse default_node; // null이면 우변 +``` + +- `?T`에서 T가 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용(크기 증가 없음). +- 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. + +### 4.6 에러 + +```fe +pub error IoError { + NotFound = 1, + Denied = 2, + Eof = 3, +} + +fn read_all(path: str) -> IoError!^[]u8 { + let f = try io.open(path); // 에러면 즉시 반환 + defer f.close(); + let n = f.size() catch |e| { return e; }; + ... +} +``` + +- `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. +- `E!T` 함수만 `try`/`catch` 사용 가능. +- `try e`: 에러면 현재 함수에서 즉시 반환(현재 함수도 에러 유니온을 반환해야 함). +- `e catch |x| { ... }`: 블록은 값을 만들거나 `return`/`break`로 탈출. +- `e catch default_value`: 짧은 형태. +- 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. +- 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. + +### 4.7 타입 동등성 + +이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. 별칭은 `const Alias = Type;`으로 만들며 완전 동일 취급. + +--- + +## 5. 소유권과 참조 — 핵심 규칙 + +이 절이 언어의 핵심이다. 모든 규칙은 **함수 하나만 보고** 검사된다. + +**R1 (단일 소유자).** 모든 값의 소유자는 정확히 하나. 변수 대입, 함수 인자 전달, 반환은 **이동(move)**이다. 이동된 변수는 이후 사용 시 컴파일 에러. + +**R2 (Copy 타입).** 다음은 이동 대신 복사된다: 정수, `bool`, `char`, raw 포인터 `*T`, 참조 `&T`, 함수 포인터, 그리고 모든 필드가 Copy이면서 `drop`이 없는 struct/enum/배열. `^T`와 `&mut T`는 Copy가 아니다. + +**R3 (소멸자, RAII).** `^T`는 소유자 스코프 종료 또는 재대입 시 `drop` 호출 후 해제. struct에 `fn drop(self: &mut Self)`가 있으면 그 값의 스코프 종료 시 자동 호출되며, 이어서 필드들의 drop이 선언 역순으로 호출된다. `drop`을 직접 호출하는 것은 컴파일 에러(`mem.destroy(x)` 사용). + +**R4 (참조는 2급 값).** `&T`, `&mut T`, `[]T`는 다음 위치에만 존재할 수 있다: +- 함수 파라미터 +- 지역 변수 (`let`/`var`) +- 표현식 안의 임시값 + +다음은 **컴파일 에러**다: +- struct/enum 필드의 타입 +- 배열/슬라이스의 원소 타입 +- 함수 반환 타입 (예외: R8) +- `^T`, `*T`의 대상 타입 +- 전역 변수의 타입 + +이 한 줄이 라이프타임 표기 전체를 불필요하게 만든다. + +**R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 전역이므로 스코프 중첩 확인만으로 검사된다. + +**R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. 참조의 생존 구간은 **참조 변수의 스코프 끝까지**(NLL 아님). 임시 참조(`f(&x)`)는 그 문장 끝까지. + +**R7 (참조 무효화).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. + +**R8 (반환값 예외).** 메서드는 `-> &T` / `-> &mut T`를 반환할 수 있다. 단, 첫 파라미터가 `&Self`/`&mut Self`이고 반환 참조는 그 self에서 파생된 것이어야 하며(컴파일러가 확인), **호출 결과는 그 문장 안에서만 사용 가능**하고 지역 변수에 바인딩할 수 없다. + +```fe +list.at(0).x = 5; // OK +let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 +``` +장수하는 접근이 필요하면 인덱스(`usize`)나 핸들을 쓴다. + +**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `asm`, `*_unchecked` 함수, 전역 `var` 접근. R1~R8은 `unsafe` 안에서도 유지되며, raw 포인터를 경유해야만 우회된다. + +**R10 (전역).** `static`은 불변이며 컴파일타임 상수 초기화만 가능. `var` 전역은 허용되나 접근이 `unsafe`. 전역에 `^T`나 `drop` 있는 타입 금지. + +**R11 (그래프 구조).** 참조 순환이 필요한 자료구조는 아레나 + 인덱스 핸들로 표현한다. 표준 라이브러리 `mem.Arena`와 `u16`/`u32` 인덱스를 쓴다. + +--- + +## 6. 문법 + +### 6.1 EBNF + +``` +unit := 'unit' ident ';' import* decl* +import := 'import' ident ';' + +decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl + | const_decl | global_decl) + +fn_decl := ['extern' string] ['interrupt'] 'fn' ident + '(' [param (',' param)*] ')' ['->' type] (block | ';') +param := ['comptime'] ident ':' type +struct_decl := ['packed'] 'struct' ident '{' field* fn_decl* '}' +field := ident ':' type ',' +enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' +variant := ident | ident '(' type ')' | ident '{' field* '}' +error_decl := 'error' ident '{' (ident '=' int ',')* '}' +const_decl := 'const' ident [':' type] '=' expr ';' +global_decl := ('static' | 'var') ident ':' type '=' expr ';' + +block := '{' stmt* '}' +stmt := 'let' ['mut'] ident [':' type] '=' expr ';' + | 'var' ident ':' type ['=' expr] ';' + | 'const' ident [':' type] '=' expr ';' + | lvalue ('=' | '+=' | '-=' | '*=' | '/=' | '%=' + | '&=' | '|=' | '^=' | '<<=' | '>>=') expr ';' + | if_stmt | while_stmt | for_stmt | match_stmt + | 'return' [expr] ';' | 'break' ';' | 'continue' ';' + | 'defer' block + | 'unsafe' block + | 'comptime' 'if' expr block ['else' (block | 'if' ...)] + | 'asm' '{' asm_body '}' + | expr ';' + +if_stmt := 'if' (expr | 'let' pattern '=' expr) block + ['else' (block | if_stmt)] +while_stmt := 'while' expr block +for_stmt := 'for' ident [',' ident] 'in' expr block +match_stmt := 'match' expr '{' arm+ '}' +arm := pattern '=>' (expr ';' | block) +pattern := ident // 배리언트, 페이로드 없음 + | ident '(' ident ')' // 튜플형 배리언트 바인딩 + | ident '{' ident (',' ident)* '}' // 필드형 배리언트 바인딩 + | 'Some' '(' ident ')' | 'None' + | int_literal | '_' + +type := ident ['.' ident] + | '?' type | '!' type | ident '!' type + | '^' type | '&' ['mut'] type | '*' type + | 'far' ('^' | '*' | '&' ['mut']) type + | '[' expr ']' type | '[' ']' type + | 'fn' '(' [type (',' type)*] ')' ['->' type] + | ident '(' type (',' type)* ')' // 제네릭 인스턴스 +``` + +### 6.2 표현식 우선순위 (낮음 → 높음) + +``` +1 orelse, catch +2 || +3 && +4 == != < <= > >= +5 | ^ +6 & +7 << >> +8 + - +% -% +9 * / % *% +10 단항: - ! ~ & &mut ^(주소아님) try +11 후위: .field .? .^ [i] [a..b] (args) as T +12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) +``` + +- `&&`, `||`는 단축 평가. +- `as`는 후위 우선순위(단항보다 강함): `-x as i32`는 `-(x as i32)`. +- 비교 연산 체이닝 금지(`a < b < c`는 에러). + +### 6.3 빌트인 + +``` +@size_of(T) -> usize @align_of(T) -> usize +@bits -> comptime int @target -> comptime str +@ptr_cast(T, p) -> *T (unsafe) +@seg_ptr(seg: u16, off: u16) -> far *T (unsafe, bits16) +@port_in8(p) @port_in16(p) @port_out8(p,v) @port_out16(p,v) (unsafe) +@volatile_load(p) @volatile_store(p, v) (unsafe) +@trap() -> never @unreachable() -> never (unsafe) +@line() @file() // 진단용 +``` + +### 6.4 예제 + +```fe +unit vga; +import sys; + +const WIDTH: u16 = 320; +const HEIGHT: u16 = 200; + +pub fn set_mode13() { + unsafe { asm { mov ax, 0x0013; int 0x10; } } +} + +pub fn put_pixel(x: u16, y: u16, c: u8) { + if x >= WIDTH or y >= HEIGHT { return; } + unsafe { + let vram: far *u8 = @seg_ptr(0xA000, 0); + @volatile_store(vram + (y * WIDTH + x) as usize, c); + } +} +``` + +```fe +unit main; +import io; +import list; +import fmt; + +fn count_lines(path: str) -> !usize { + let f = try io.open(path, io.Read); + defer f.close(); + + var buf: [256]u8 = undefined; + var n: usize = 0; + while true { + let got = try f.read(buf[..]); + if got == 0 { break; } + for c in buf[0..got] { + if c.^ == '\n' { n += 1; } + } + } + return n; +} + +pub fn main() -> !void { + let n = count_lines("data.txt") catch |e| { + fmt.print_str("failed: "); + fmt.print_int(e as u16); + return e; + }; + fmt.print_int(n); +} +``` + +--- + +## 7. 의미론 세부 + +### 7.1 변수와 초기화 + +- `let`은 불변, `let mut`은 가변, `var`는 타입 명시 필수인 가변 선언. +- 모든 변수는 사용 전 초기화 필수(정적 검사). 명시적 미초기화는 `= undefined`(unsafe 아님, 단 읽기 전 쓰기 필수는 여전히 검사). +- 섀도잉 허용(같은 스코프에서 `let` 재선언). + +### 7.2 제어 흐름 + +- `for x in slice`: `x`는 `&T`(가변 슬라이스면 `&mut T`). 값 접근은 `x.^`. +- `for i, x in slice`: `i: usize`. +- `for i in a..b`: 정수 범위. +- 이 루프 형태들은 경계 검사를 생략한다(컴파일러가 안전을 보장). +- `while`은 `bool` 조건만. +- `match`는 **완전성 검사**. 모든 배리언트를 다루거나 `_` 필요. +- `break`/`continue`는 가장 안쪽 루프에만 적용(레이블 없음). +- `defer block`은 스코프 종료 시 역순 실행. 소멸자와 함께 선언 역순으로 병합 실행. `return`/`break`/에러 전파 경로에서도 실행. + +### 7.3 함수 호출 규약 + +- 기본: `bits32`는 cdecl, `bits16`은 타깃 C 컴파일러 기본. +- `extern "c" fn name(...) -> T;` — 본문 없이 선언, C 심볼과 링크. 이름 맹글링 없음. 인자/반환에 `^T`, 슬라이스, 에러 유니온 사용 불가(`*T`, `usize`만). +- `interrupt fn name()` — 모든 레지스터 보존 + `iret`. 파라미터/반환 없음. 주소는 `@as_far_fn(name)`으로 획득. +- 큰 struct(> 4바이트)는 숨은 포인터로 반환(C ABI 따름). + +### 7.4 검사와 트랩 + +트랩 발생 조건: 배열/슬라이스 경계 초과, 정수 오버플로, 0 나눗셈, `?T`의 `.?` 실패, `@trap()`. + +동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. + +`--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. +**절대 제거되지 않는 것:** 소유권/참조 검사, 옵셔널 타입 검사, `match` 완전성 — 전부 컴파일타임이므로. + +### 7.5 comptime + +- `const` 선언의 초기값은 컴파일타임 평가(정수 연산, `@size_of`, `@bits`, 다른 const). +- `comptime if`는 평가되지 않는 분기를 **파싱은 하되 타입 검사/코드 생성하지 않는다**(타깃별 분기용). +- 함수의 `comptime` 파라미터는 §9 제네릭. +- 재귀 평가 깊이 제한 256, 초과 시 에러. + +--- + +## 8. 유닛과 빌드 + +- 파일 하나 = 유닛 하나. 첫 줄은 `unit <이름>;`이며 파일명과 일치해야 함. +- `import bar;` → 같은 검색 경로의 `bar.fe`. 접근은 `bar.name`. +- `pub` 붙은 선언만 외부 노출. 구조체 필드도 개별 `pub` 필요. +- 순환 import 금지(에러). +- 유닛 컴파일 시 `.fei` 생성: pub 선언 시그니처, 타입 레이아웃, 제네릭 본문 토큰. 소스 해시가 같으면 재컴파일 생략. +- 검색 경로: `-I `, 기본은 현재 디렉터리 + `/std`. + +``` +fec main.fe --target=bits32 -o game.exe +fec main.fe --target=bits16 --model=large --no-checks -o game.exe +fec main.fe --emit-c -o out/ # 트랜스파일 결과만 +fec --dump-ast main.fe +``` + +--- + +## 9. 제네릭 + +`comptime` 파라미터 기반 모노모피제이션. + +```fe +pub struct List(T) { + items: ^[]T, + len: usize, + + pub fn new() -> List(T) { ... } + pub fn push(self: &mut Self, v: T) -> !void { ... } + pub fn at(self: &Self, i: usize) -> &T { ... } // R8 적용 + pub fn drop(self: &mut Self) { ... } +} + +fn max(comptime T: type, a: T, b: T) -> T { + if a > b { return a; } + return b; +} + +let m = max(i32, 3, 7); +var xs: List(u8) = List(u8).new(); +``` + +- 인스턴스화 시 타입 인자를 대입해 본문을 재검사하고 코드를 생성한다. 인스턴스 캐시 키는 `(선언, 타입 인자 목록)`. +- 제약(trait bound) 없음. 본문에서 쓰는 연산이 그 타입에 없으면 **인스턴스화 시점에** 에러(에러 메시지에 인스턴스화 위치를 표시할 것). +- `.fei`에 제네릭 본문을 토큰 스트림으로 저장, 사용처에서 재파싱. +- 재귀적 인스턴스화 깊이 제한 32. + +--- + +## 10. 표준 라이브러리 (최소 집합) + +- **core**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. +- **mem**: `create(T) -> !^T`, `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `copy(dst, src)`, `set(dst, v)`, `Arena{ init, alloc, reset, drop }`. +- **str**: `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr(buf, s)`, `from_cstr(p)`. +- **list**: `List(T)`. +- **map**: `Map(K, V)` (오픈 어드레싱, `K`는 정수/str). +- **fmt**: `print_str`, `print_int`, `print_hex`, `format(buf, ...)`. 제네릭 도입 후 `print(fmt, args)` 추가. +- **io**: `File{ open, create, read, write, seek, size, close(=drop) }`, `stdin`, `stdout`, `stderr`. +- **sys**: `exit`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). + +`io.File`은 `drop`에서 핸들을 닫는다. 이중 닫기는 소유권 규칙이 막는다. + +--- + +## 11. 컴파일러 구현 + +### 11.1 부트스트랩 전략 + +1. **컴파일러 A** — C89로 작성. Ferro → C 트랜스파일러. 호스트는 현대 PC 또는 DOS. 출력 C는 DJGPP(gcc, bits32) / Open Watcom(bits16, bits32) / Borland C(bits16)로 컴파일. +2. **컴파일러 B** — Ferro로 A와 동일 구조를 재작성. A로 빌드. +3. **셀프호스팅** — B로 B를 빌드. 그 결과로 다시 B를 빌드해 출력이 바이트 동일(fixpoint)하면 완료. A 폐기. +4. **네이티브 백엔드** — B에 386 코드 생성기 추가, 이후 8086 코드 생성기. + +A는 버릴 코드다. 최적화하지 말고 B를 컴파일할 수 있는 최소 언어 부분집합만 지원한다. + +### 11.2 파이프라인 + +``` +소스 → lexer → parser(AST) → resolve(이름/import) → check(타입) + → own(소유권·참조) → lower(소멸자/defer/try/for 전개 → LIR) + → emit_c(C 소스) [또는 emit_x86] +``` + +각 단계는 실패해도 가능한 한 진행해 에러를 모아 보고한다(문장 단위 복구). + +### 11.3 디렉터리 + +``` +fec/ + src/ + lexer.c/h 토큰화. 위치(파일, 줄, 열) 보존. + ast.c/h 노드 정의, 아레나 할당자. + parser.c/h LL(1) 재귀하강. 에러 복구는 다음 ';' 또는 '}'까지 스킵. + types.c/h 타입 인터닝(포인터 비교로 동등성), 레이아웃 계산(타깃별). + resolve.c/h 스코프 체인, 심볼 테이블, import, .fei 읽기/쓰기. + check.c/h 타입 검사, 리터럴 타입 결정, match 완전성, R4 위치 검사. + own.c/h §11.5 알고리즘. + lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개. + emit_c.c/h LIR → C. §11.4 규칙. + generic.c/h 인스턴스 캐시, 토큰 재파싱. + driver.c CLI, 유닛 의존 순서, .fei 캐시, 외부 C 컴파일러 호출. + rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim + std/ 표준 라이브러리 (.fe) + tests/ §12 +``` + +### 11.4 C 방출 규칙 + +| Ferro | C | +|---|---| +| `i16`, `u32` 등 | `int16_t`, `uint32_t` (`` 없으면 자체 typedef) | +| `usize` | `uint16_t`(bits16) / `uint32_t`(bits32) | +| `bool` | `unsigned char` | +| `^T`, `*T` | `T*` | +| `&T` | `const T*` | +| `&mut T` | `T*` | +| `far X` | `__far X` (Watcom/Borland), bits32는 무시 | +| `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) | +| `[]T` | `typedef struct { T* p; fe_usize n; } fe_slice_T;` | +| `?T` (포인터류) | 원래 포인터, null 사용 | +| `?T` (그 외) | `struct { unsigned char has; T v; }` | +| `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | +| struct | `struct fe__` | +| enum | `struct { uint8_t tag; union { ... } u; }` | +| 함수 | `fe__`, 메서드는 `fe___` | +| 제네릭 인스턴스 | `fe____<타입인자맹글>` | + +세부: +- **오버플로 검사**: `fe_add_i16(a, b, LINE)` 인라인 함수. `--no-checks`면 매크로가 `((a)+(b))`로 축약. +- **경계 검사**: `fe_idx_T(s, i, LINE)` → `(i < s.n ? s.p[i] : (fe_trap_bounds(LINE), s.p[0]))`. `for` 루프는 직접 인덱스. +- **`try`**: `{ Ttmp t = expr; if (t.e) return (RetT){ t.e }; }` 후 `t.v` 사용. defer/소멸자가 있으면 return 전에 정리 코드 삽입. +- **`catch`**: `t.e`가 참일 때 블록 실행, 바인딩 변수는 `t.e`. +- **`defer`/소멸자**: lower 단계에서 스코프 종료 지점(정상 흐름, `return`, `break`, `continue`, `try` 전파)마다 역순 호출을 명시적으로 삽입. C의 goto 라벨을 써도 되고 복제해도 된다(A는 복제, B는 goto 권장). +- **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. +- **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. +- **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. +- **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. +- 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성. + +### 11.5 own.c 알고리즘 + +함수 단위. 각 지역 변수/파라미터에 상태: +``` +Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive +``` + +1. AST를 문장 순서로 순회하며 상태 전이. +2. 표현식 평가 시 lvalue 사용을 분류: 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기. +3. 이동: `Owned → Moved`. `Moved`/`MaybeMoved` 사용 시 에러(최초 이동 위치를 에러에 표시). +4. `&x`: `Owned → Shared(n+1)`. `&mut x`: `Owned → Exclusive`. 해제는 참조 변수의 스코프 끝(임시 참조는 문장 끝). +5. `Shared`/`Exclusive` 상태에서 쓰기/이동/재대여 시 에러(R6). +6. **분기 합류**: `if`/`match`의 각 브랜치를 독립 상태로 계산 후 병합. `Owned` + `Moved` → `MaybeMoved`(사용 에러, drop은 런타임 플래그). +7. **루프**: 본문을 2회 순회. 1회차 종료 상태를 진입 상태와 병합해 2회차 실행, 상태가 수렴하지 않으면(예: 첫 반복에서 이동) 에러. +8. R4 위반(참조를 필드/반환/힙에 저장)은 own이 아니라 check 단계에서 **타입만 보고** 거부한다. +9. R8(메서드 참조 반환)은 호출 결과를 바인딩하려는 시도를 check에서 거부. + +에러 메시지 형식: `file:line:col: error: <설명>` + 관련 위치 `file:line:col: note: <최초 이동/대여 위치>`. + +### 11.6 마일스톤 + +| # | 내용 | 완료 기준 | +|---|---|---| +| M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 | +| M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 | +| M3 | struct, enum, match, 배열, 슬라이스, 경계 검사, str | 문자열 처리 예제 통과 | +| M4 | `^T`, drop, defer, 이동 검사 | 누수/이중해제 테스트 통과 | +| M5 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | +| M6 | `?T`, `E!T`, try/catch | io 유닛 동작 | +| M7 | 유닛/import/.fei, 분리 컴파일, std 초안 | 다중 유닛 프로그램 빌드 | +| M8 | **제네릭** (모노모피제이션) | `List(T)`, `Map(K,V)`를 Ferro로 재작성 | +| M9 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn | DOSBox에서 VGA 데모 실행 | +| M10 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M9 테스트 통과 | +| M11 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | +| M12 | 386 네이티브 백엔드 | gcc 없이 빌드, 컴파일 속도 10배 | +| M13 | 8086 네이티브 백엔드 | Watcom 없이 bits16 빌드 | + +M8은 M9보다 앞이다(제네릭 없이 표준 라이브러리를 쓰는 기간을 최소화). + +--- + +## 12. 테스트 + +``` +tests/ + pass/*.fe + *.expected 컴파일→실행→stdout 비교 + fail/*.fe 첫 줄 "// ERROR::<메시지 일부>" + run16/*.fe bits16 빌드 후 DOSBox 실행, 출력 파일 비교 + boot/ A/B 출력 비교, fixpoint 검증 +``` + +- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조 반환 바인딩), R9(unsafe 밖 raw 역참조), match 완전성, 암묵 변환, 타입 불일치. +- 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. +- 회귀 실행: `make test` — 전 타깃 전 테스트. + +--- + +## 13. 의도적으로 제외한 기능 + +| 기능 | 제외 이유 | 대체 수단 | +|---|---|---| +| 라이프타임 표기 (`'a`) | 전역 분석 필요 | R4 (2급 참조) | +| 트레잇/인터페이스 | 복잡도 대비 이득 낮음 | 함수 포인터 struct: `struct Writer { ctx: *void, write: fn(*void, []u8) -> !usize }` | +| 클로저 | 캡처 = 참조 저장 = R4 위반 | 콜백에 `ctx: *void` 전달 | +| 매크로 / 전처리기 | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭 | +| 예외 | 언와인딩 기반 시설 없음, 비용 큼 | 에러 유니온 | +| GC | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 | +| 연산자 오버로딩 | 숨은 비용 | 메서드 | +| 가변 인자 | ABI 복잡, 타입 안전 불가 | 제네릭 `comptime` 파라미터 | +| 튜플 / 다중 반환 | 이름 없는 필드는 가독성 손해 | struct | +| 암묵 형변환 | 버그 원인 1위 | `as` | +| 스레드 | DOS에 없음 | — | +| 상속 | — | 합성 | + diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..faeb710 --- /dev/null +++ b/TODO.md @@ -0,0 +1,5 @@ +# TODO + +- [x] Add a non-reboot abort path for a hung DOS command: inject `Ctrl+C` through QEMU's monitor and wait for the serial agent to recover. +- [x] Apply a configurable timeout to `dos_exec` and invoke the non-reboot abort path on timeout. +- [x] Expose `dos_abort` for an immediate user-requested command interruption. From c7b0217d43d0a8f8ffc418ac0d0d47ba430f79c1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 06:16:03 +0900 Subject: [PATCH 002/184] docs: define formatting builtins and interface roadmap --- SPEC.md | 135 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 108 insertions(+), 27 deletions(-) diff --git a/SPEC.md b/SPEC.md index 746cc2c..ecc9e02 100644 --- a/SPEC.md +++ b/SPEC.md @@ -306,8 +306,36 @@ type := ident ['.' ident] @volatile_load(p) @volatile_store(p, v) (unsafe) @trap() -> never @unreachable() -> never (unsafe) @line() @file() // 진단용 + +@print(fmt, ...) // stdout +@fprint(w, fmt, ...) // 임의 Writer +@sprint(buf: []u8, fmt, ...) -> usize // 버퍼에 기록, 쓴 바이트 수 반환 ``` +### 6.3.1 포매팅 빌트인 + +가변 인자를 언어에 도입하지 않는다. `@print` 계열은 **컴파일 단계에서 여러 호출로 전개되는 빌트인**이다. + +```fe +@print("x={} y={x} name={s}\n", a, b, s); +``` +→ lower 단계에서 다음으로 전개: +``` +fmt.write_str(out, "x="); fmt.write_int_i32(out, a); +fmt.write_str(out, " y="); fmt.write_hex_u16(out, b); +fmt.write_str(out, " name="); fmt.write_str(out, s); +fmt.write_str(out, "\n"); +``` + +규칙: +- 포맷 문자열은 **컴파일타임 문자열 리터럴 또는 `const`만**. 런타임 값이면 에러. +- verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. +- `{}` 개수와 인자 개수 불일치 → 컴파일 에러. +- 인자 타입에 대응하는 `fmt.write_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). +- 자릿수/폭/정렬 지정자는 v0.1에 없음. 필요하면 `fmt.write_int_pad`를 직접 호출. +- `@fprint`의 첫 인자는 `&mut io.Writer`(§10의 함수 포인터 struct). +- 전개 결과의 모든 호출은 `!void`를 반환하므로 `@print`도 `!void`. 무시하려면 `_ = @print(...)`. + ### 6.4 예제 ```fe @@ -464,8 +492,25 @@ var xs: List(u8) = List(u8).new(); - **str**: `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr(buf, s)`, `from_cstr(p)`. - **list**: `List(T)`. - **map**: `Map(K, V)` (오픈 어드레싱, `K`는 정수/str). -- **fmt**: `print_str`, `print_int`, `print_hex`, `format(buf, ...)`. 제네릭 도입 후 `print(fmt, args)` 추가. -- **io**: `File{ open, create, read, write, seek, size, close(=drop) }`, `stdin`, `stdout`, `stderr`. +- **fmt**: `@print` 계열이 전개해 호출하는 저수준 함수 모음. + `write_str(w, s)`, `write_int_i8/i16/i32/u8/u16/u32(w, v)`, `write_hex_u8/u16/u32(w, v)`, + `write_char(w, c)`, `write_bool(w, b)`, `write_int_pad(w, v, width, pad)`. + 전부 `fn(w: &mut io.Writer, ...) -> !void` 시그니처. 사용자가 직접 호출해도 된다. +- **io**: + - `File{ open, create, read, write, seek, size, close(=drop) }`, `stdin`, `stdout`, `stderr`. + - `Writer` — 함수 포인터 struct (인터페이스 도입 전까지의 동적 디스패치 수단): + ```fe + pub struct Writer { + ctx: *void, + write_fn: fn(*void, []u8) -> !usize, + } + ``` + `File.writer(&mut self) -> Writer`, `buf_writer(buf: &mut []u8) -> Writer`, `null_writer()` 제공. + - `Reader` — 같은 형태, `read_fn: fn(*void, []u8) -> !usize`. + - `Writer`/`Reader`는 `*void`를 담으므로 필드 저장이 가능(2급 참조 아님). 대신 대상보다 오래 살면 + dangling이므로 **`Writer`를 만든 지역 스코프 밖으로 내보내지 않는 것**이 사용자 책임이며, + `writer()` 메서드는 R8(참조 반환) 대상이 아니라 값 반환이라 컴파일러가 막지 않는다. + v0.2에서 `dyn Writer`로 대체되면 이 구멍이 닫힌다. - **sys**: `exit`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). `io.File`은 `drop`에서 핸들을 닫는다. 이중 닫기는 소유권 규칙이 막는다. @@ -544,6 +589,7 @@ fec/ - **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. - **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. - **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. +- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. - **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. - 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성. @@ -573,18 +619,22 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive | M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 | | M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 | | M3 | struct, enum, match, 배열, 슬라이스, 경계 검사, str | 문자열 처리 예제 통과 | -| M4 | `^T`, drop, defer, 이동 검사 | 누수/이중해제 테스트 통과 | -| M5 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | -| M6 | `?T`, `E!T`, try/catch | io 유닛 동작 | -| M7 | 유닛/import/.fei, 분리 컴파일, std 초안 | 다중 유닛 프로그램 빌드 | -| M8 | **제네릭** (모노모피제이션) | `List(T)`, `Map(K,V)`를 Ferro로 재작성 | -| M9 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn | DOSBox에서 VGA 데모 실행 | -| M10 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M9 테스트 통과 | -| M11 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | -| M12 | 386 네이티브 백엔드 | gcc 없이 빌드, 컴파일 속도 10배 | -| M13 | 8086 네이티브 백엔드 | Watcom 없이 bits16 빌드 | +| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), `io.Writer` 함수 포인터 struct | 포맷 출력 동작, 인자 개수·타입 불일치가 컴파일 에러 | +| M5 | `^T`, drop, defer, 이동 검사 | 누수/이중해제 테스트 통과 | +| M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | +| M7 | `?T`, `E!T`, try/catch | io 유닛 동작 | +| M8 | 유닛/import/.fei, 분리 컴파일, std 초안 | 다중 유닛 프로그램 빌드 | +| M9 | **제네릭** (모노모피제이션) | `List(T)`, `Map(K,V)`를 Ferro로 재작성 | +| M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn | DOSBox에서 VGA 데모 실행 | +| M11 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M10 테스트 통과 | +| M12 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | +| M13 | 386 네이티브 백엔드 | gcc 없이 빌드, 컴파일 속도 10배 | +| M14 | 8086 네이티브 백엔드 | Watcom 없이 bits16 빌드 | -M8은 M9보다 앞이다(제네릭 없이 표준 라이브러리를 쓰는 기간을 최소화). +배치 근거: +- **M4(포매팅)를 앞에 두는 이유**: 구현이 작고(check + lower 합쳐 300줄 안팎) 언어 표면에 새 개념을 추가하지 않는다. 이후 모든 마일스톤의 디버깅과 M11의 컴파일러 B 에러 출력이 여기에 의존한다. +- **M9(제네릭)가 M10보다 앞인 이유**: 제네릭 없이 표준 라이브러리를 쓰는 기간을 최소화한다. +- **인터페이스(`dyn`)는 마일스톤에 없다**: 부트스트랩 경로에 불필요하고(컴파일러 B는 인터페이스 없이 작성 가능), 타입 시스템 전반에 영향을 준다. §13의 v0.2 1순위로 미룬다. 그때까지 `io.Writer`/`io.Reader` 함수 포인터 struct로 대체한다. --- @@ -599,6 +649,8 @@ tests/ ``` - `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조 반환 바인딩), R9(unsafe 밖 raw 역참조), match 완전성, 암묵 변환, 타입 불일치. +- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`. +- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@sprint` 반환 길이 검증, `@fprint`를 `io.buf_writer`로 호출. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. - 회귀 실행: `make test` — 전 타깃 전 테스트. @@ -606,18 +658,47 @@ tests/ ## 13. 의도적으로 제외한 기능 -| 기능 | 제외 이유 | 대체 수단 | -|---|---|---| -| 라이프타임 표기 (`'a`) | 전역 분석 필요 | R4 (2급 참조) | -| 트레잇/인터페이스 | 복잡도 대비 이득 낮음 | 함수 포인터 struct: `struct Writer { ctx: *void, write: fn(*void, []u8) -> !usize }` | -| 클로저 | 캡처 = 참조 저장 = R4 위반 | 콜백에 `ctx: *void` 전달 | -| 매크로 / 전처리기 | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭 | -| 예외 | 언와인딩 기반 시설 없음, 비용 큼 | 에러 유니온 | -| GC | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 | -| 연산자 오버로딩 | 숨은 비용 | 메서드 | -| 가변 인자 | ABI 복잡, 타입 안전 불가 | 제네릭 `comptime` 파라미터 | -| 튜플 / 다중 반환 | 이름 없는 필드는 가독성 손해 | struct | -| 암묵 형변환 | 버그 원인 1위 | `as` | -| 스레드 | DOS에 없음 | — | -| 상속 | — | 합성 | +**등급 정의** +- `영구` — §1 철학과 정면 충돌. v2.0에서도 넣지 않는다. +- `구조적 불가` — 넣으면 R4를 풀어야 하고 전역 분석이 생겨 셀프호스팅 목표가 깨진다. 이 언어의 정의상 불가. +- `v0.2` — 넣을 예정. 순서 문제일 뿐 원칙 위반 아님. +- `편의` — 원칙 위반 없음, 구현도 쉬움. 여유 생기면 아무 때나. +| 기능 | 등급 | 제외 이유 | 대체 수단 | +|---|---|---|---| +| 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | 함수 포인터 struct (`io.Writer`, §10) | +| 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 | +| 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 | +| 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | +| 레이블 있는 break | 편의 | — | 플래그 변수 | +| 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | +| `inline fn` | 편의 | — | C 방출 시 `static inline` | +| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), 인덱스 핸들 | +| 스레드 | 구조적 불가 | DOS에 없음 | — | +| 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` | +| 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 | +| GC | 영구 | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 | +| 암묵 형변환 | 영구 | 버그 원인 1위 | `as` | +| 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 | + +### 13.1 인터페이스 설계 스케치 (v0.2 예정) + +지금 구현하지 않되, 나중에 `io.Writer` 함수 포인터 struct를 무리 없이 대체할 수 있도록 방향만 고정해 둔다. + +```fe +pub interface Writer { + fn write(self: &mut Self, buf: []u8) -> !usize; +} +impl Writer for File { ... } + +fn dump(w: &mut dyn Writer, data: []u8) -> !void { ... } +dump(&mut file, buf); // &mut File → &mut dyn Writer 자동 변환 +``` + +- `dyn I`의 표현은 `(ctx, vtable)` 팻 포인터. vtable은 `(인터페이스, 구현 타입)` 쌍마다 `static const` 하나. +- **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다. +- `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱). +- `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립 마일스톤으로 다룬다. +- 도입 시 `io.Writer`/`io.Reader`는 `dyn`으로 교체하고, 함수 포인터 struct 버전은 제거한다(`*void`가 만드는 dangling 구멍이 닫힌다). + +위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토. From 990068f4245cee73280375cf75b564bd3726a676 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 06:44:39 +0900 Subject: [PATCH 003/184] docs: resolve language audit for Ferro v0.1.2 --- SPEC.md | 113 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 32 deletions(-) diff --git a/SPEC.md b/SPEC.md index ecc9e02..b4d387e 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1 +# Ferro 언어 명세 v0.1.2 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -28,7 +28,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 | 메모리 모델 | small, large | flat | | 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | -- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large]` +- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. - 표준 라이브러리는 코어 공용, `sys` 유닛만 타깃별 구현. @@ -47,10 +47,11 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 **예약어:** ``` -unit import pub fn struct enum error const static var let mut +unit import pub fn struct packed enum error const static var let if else while for in match return break continue defer -unsafe comptime asm try catch as extern interrupt far -true false null self Self type +unsafe critical shared atomic comptime asm try catch as extern interrupt interrupt_safe far +true false null undefined self Self type +and or not orelse ``` --- @@ -164,6 +165,21 @@ fn read_all(path: str) -> IoError!^[]u8 { - `e catch default_value`: 짧은 형태. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. +- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. + +`error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 +멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. +최종 빌드에서 모든 유닛의 이름을 합치고 중복을 제거한 뒤 이름의 바이트순으로 +정렬하여 1부터 안정적인 `u16` 코드를 부여한다. 따라서 서로 다른 유닛의 +`error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이다. +코드는 최종 링크용 생성 헤더의 심볼로 참조하므로 분리 컴파일에서도 일관된다. +이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal +타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 +변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 +축약형 `!T`를 반환하는 함수에서만 직접 반환할 수 있다. +`--strip-error-names`를 사용하면 실행 파일과 런타임 오류 문자열에서 이름을 +제거하지만 숫자 코드와 `.fei`의 타입/코드 일관성 정보는 유지한다. +`fmt.write_error`는 이 정책에 따라 `core.Error` 값을 이름 또는 코드로 출력한다. ### 4.7 타입 동등성 @@ -179,7 +195,7 @@ fn read_all(path: str) -> IoError!^[]u8 { **R2 (Copy 타입).** 다음은 이동 대신 복사된다: 정수, `bool`, `char`, raw 포인터 `*T`, 참조 `&T`, 함수 포인터, 그리고 모든 필드가 Copy이면서 `drop`이 없는 struct/enum/배열. `^T`와 `&mut T`는 Copy가 아니다. -**R3 (소멸자, RAII).** `^T`는 소유자 스코프 종료 또는 재대입 시 `drop` 호출 후 해제. struct에 `fn drop(self: &mut Self)`가 있으면 그 값의 스코프 종료 시 자동 호출되며, 이어서 필드들의 drop이 선언 역순으로 호출된다. `drop`을 직접 호출하는 것은 컴파일 에러(`mem.destroy(x)` 사용). +**R3 (소멸자, RAII).** `^T`는 소유자 스코프 종료 또는 재대입 시 `drop` 호출 후 해제. struct에 `fn drop(self: &mut Self)`가 있으면 그 값의 스코프 종료 시 자동 호출되며, 이어서 필드들의 drop이 선언 역순으로 호출된다. `drop`을 직접 호출하는 것은 컴파일 에러(`mem.destroy(x)` 사용). `^Self` 또는 `?^Self`를 재귀적으로 포함한 타입은 기본 필드 drop이 스택 깊이에 비례할 수 있으므로 컴파일러가 경고한다. 이런 연결 구조는 `mem.replace(&mut link, null)`로 소유 링크를 하나씩 꺼내 반복 해제하고 필드를 빈 값으로 남기는 사용자 `drop`을 정의해야 하며, `--deny-recursive-drop`으로 경고를 에러로 바꿀 수 있다. **R4 (참조는 2급 값).** `&T`, `&mut T`, `[]T`는 다음 위치에만 존재할 수 있다: - 함수 파라미터 @@ -209,11 +225,16 @@ let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 ``` 장수하는 접근이 필요하면 인덱스(`usize`)나 핸들을 쓴다. -**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `asm`, `*_unchecked` 함수, 전역 `var` 접근. R1~R8은 `unsafe` 안에서도 유지되며, raw 포인터를 경유해야만 우회된다. +**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `@as_far_fn`, `@call_far`, `asm`, `*_unchecked` 함수. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. -**R10 (전역).** `static`은 불변이며 컴파일타임 상수 초기화만 가능. `var` 전역은 허용되나 접근이 `unsafe`. 전역에 `^T`나 `drop` 있는 타입 금지. +**R10 (전역과 인터럽트 공유).** `static`은 불변이며 컴파일타임 상수 초기화만 가능하다. 일반 전역 `var`의 읽기와 쓰기는 안전하며 `unsafe`가 필요 없다. 인터럽트 핸들러와 메인 흐름이 함께 접근하는 값은 반드시 `shared var`로 선언하고 다음 규칙을 적용한다. +- 메인 흐름은 `critical {}` 안에서만 `shared var`에 접근할 수 있다. 진입 시 플래그를 저장하고 인터럽트를 막으며, 정상 종료·`return`·`break`·`continue`·에러 전파를 포함한 모든 이탈 경로에서 원래 플래그를 복원한다. +- `interrupt fn` 안에서는 `shared var`에 직접 접근할 수 있다. 일반 함수와 `interrupt_safe fn`은 호출 문맥을 알 수 없으므로 명시적 `critical` 밖의 비원자 공유 접근이 금지된다. +- `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/포인터 스칼라에만 허용한다. 메인 흐름의 단일 읽기·쓰기에는 컴파일러가 필요한 최소 임계 구역을 생성한다. 복합 read-modify-write는 여전히 명시적 `critical {}`이 필요하다. 지원되지 않는 크기나 타입은 컴파일 에러다. +- `interrupt fn`은 `interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점, `critical` 및 안전하지 않은 함수 호출을 사용할 수 없으며 컴파일러가 함수 본문만 보고 검증한다. 이 효과는 `.fei` 시그니처에 기록한다. +- 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1.2에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. -**R11 (그래프 구조).** 참조 순환이 필요한 자료구조는 아레나 + 인덱스 핸들로 표현한다. 표준 라이브러리 `mem.Arena`와 `u16`/`u32` 인덱스를 쓴다. +**R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. --- @@ -228,7 +249,7 @@ import := 'import' ident ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl | const_decl | global_decl) -fn_decl := ['extern' string] ['interrupt'] 'fn' ident +fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type struct_decl := ['packed'] 'struct' ident '{' field* fn_decl* '}' @@ -237,11 +258,13 @@ enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' variant := ident | ident '(' type ')' | ident '{' field* '}' error_decl := 'error' ident '{' (ident '=' int ',')* '}' const_decl := 'const' ident [':' type] '=' expr ';' -global_decl := ('static' | 'var') ident ':' type '=' expr ';' +global_decl := 'static' ident ':' type '=' expr ';' + | 'var' ident ':' type '=' expr ';' + | 'shared' ['atomic'] 'var' ident ':' type '=' expr ';' block := '{' stmt* '}' -stmt := 'let' ['mut'] ident [':' type] '=' expr ';' - | 'var' ident ':' type ['=' expr] ';' +stmt := 'let' ident [':' type] '=' expr ';' + | 'var' ident [':' type] ['=' expr] ';' | 'const' ident [':' type] '=' expr ';' | lvalue ('=' | '+=' | '-=' | '*=' | '/=' | '%=' | '&=' | '|=' | '^=' | '<<=' | '>>=') expr ';' @@ -249,6 +272,7 @@ stmt := 'let' ['mut'] ident [':' type] '=' expr ';' | 'return' [expr] ';' | 'break' ';' | 'continue' ';' | 'defer' block | 'unsafe' block + | 'critical' block | 'comptime' 'if' expr block ['else' (block | 'if' ...)] | 'asm' '{' asm_body '}' | expr ';' @@ -269,6 +293,7 @@ type := ident ['.' ident] | '?' type | '!' type | ident '!' type | '^' type | '&' ['mut'] type | '*' type | 'far' ('^' | '*' | '&' ['mut']) type + | 'far' 'fn' '(' [type (',' type)*] ')' ['->' type] | '[' expr ']' type | '[' ']' type | 'fn' '(' [type (',' type)*] ')' ['->' type] | ident '(' type (',' type)* ')' // 제네릭 인스턴스 @@ -278,20 +303,21 @@ type := ident ['.' ident] ``` 1 orelse, catch -2 || -3 && +2 or +3 and 4 == != < <= > >= 5 | ^ 6 & 7 << >> 8 + - +% -% 9 * / % *% -10 단항: - ! ~ & &mut ^(주소아님) try +10 단항: - not ~ & &mut ^(주소아님) try 11 후위: .field .? .^ [i] [a..b] (args) as T 12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) ``` -- `&&`, `||`는 단축 평가. +- `and`, `or`는 단축 평가한다. 호스트 C 방출에서는 각각 `&&`, `||`로 + 매핑하며, 평가 순서와 단락 규칙은 Ferro 의미론을 그대로 유지한다. - `as`는 후위 우선순위(단항보다 강함): `-x as i32`는 `-(x as i32)`. - 비교 연산 체이닝 금지(`a < b < c`는 에러). @@ -307,9 +333,12 @@ type := ident ['.' ident] @trap() -> never @unreachable() -> never (unsafe) @line() @file() // 진단용 -@print(fmt, ...) // stdout -@fprint(w, fmt, ...) // 임의 Writer +@print(fmt, ...) -> void // stdout, 쓰기 오류 무시 +@fprint(w, fmt, ...) -> !void // 임의 Writer @sprint(buf: []u8, fmt, ...) -> usize // 버퍼에 기록, 쓴 바이트 수 반환 +@compile_error(msg) // comptime에서 항상 컴파일 에러 +@as_far_fn(f) -> far fn() // bits16 전용 함수 포인터 변환 +@call_far(p: far fn()) // bits16/unsafe 전용 호출 ``` ### 6.3.1 포매팅 빌트인 @@ -332,9 +361,21 @@ fmt.write_str(out, "\n"); - verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. - `{}` 개수와 인자 개수 불일치 → 컴파일 에러. - 인자 타입에 대응하는 `fmt.write_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). -- 자릿수/폭/정렬 지정자는 v0.1에 없음. 필요하면 `fmt.write_int_pad`를 직접 호출. +- 자릿수/폭/정렬 지정자는 v0.1.1에 없음. 필요하면 `fmt.write_int_pad`를 직접 호출. - `@fprint`의 첫 인자는 `&mut io.Writer`(§10의 함수 포인터 struct). -- 전개 결과의 모든 호출은 `!void`를 반환하므로 `@print`도 `!void`. 무시하려면 `_ = @print(...)`. +- `@print`는 stdout에 기록하며 저수준 writer 오류를 삼키고 `void`를 반환한다. + 따라서 `try @print(...)`는 컴파일 에러다. +- `@fprint`는 writer 오류를 전파하여 `!void`를 반환한다. +- `@sprint`는 버퍼가 찬 뒤의 출력이 잘리더라도 트랩하지 않고 기록된 바이트 수를 + `usize`로 반환한다. +- 전개된 `fmt.write_*` 호출은 위 반환 규칙에 맞게 lower 단계에서 오류를 + 전파하거나 무시한다. `fmt.write_error`는 `core.Error`의 이름/코드를 출력한다. + +`@compile_error(msg)`의 `msg`는 comptime 문자열이어야 하며, 평가되는 분기에서 +항상 진단을 발생시킨다. `comptime if`의 제거되는 분기에서는 진단하지 않는다. +`@as_far_fn(f)`와 `@call_far`는 `bits16`에서만 허용된다. 전자는 함수 포인터를 +`far fn()`으로 변환하고 후자는 `far fn()`을 호출한다. 둘 다 `unsafe { }` 안에서만 +사용할 수 있으며, `bits32`에서는 컴파일 에러다. ### 6.4 예제 @@ -396,7 +437,7 @@ pub fn main() -> !void { ### 7.1 변수와 초기화 -- `let`은 불변, `let mut`은 가변, `var`는 타입 명시 필수인 가변 선언. +- `let`은 불변, `var`는 가변 선언이다. 두 형태 모두 초기값이 있으면 타입을 추론할 수 있다. `var x: T;`와 `var x: T = undefined;`처럼 초기값이 없거나 `undefined`이면 타입 명시가 필수다. - 모든 변수는 사용 전 초기화 필수(정적 검사). 명시적 미초기화는 `= undefined`(unsafe 아님, 단 읽기 전 쓰기 필수는 여전히 검사). - 섀도잉 허용(같은 스코프에서 `let` 재선언). @@ -415,7 +456,8 @@ pub fn main() -> !void { - 기본: `bits32`는 cdecl, `bits16`은 타깃 C 컴파일러 기본. - `extern "c" fn name(...) -> T;` — 본문 없이 선언, C 심볼과 링크. 이름 맹글링 없음. 인자/반환에 `^T`, 슬라이스, 에러 유니온 사용 불가(`*T`, `usize`만). -- `interrupt fn name()` — 모든 레지스터 보존 + `iret`. 파라미터/반환 없음. 주소는 `@as_far_fn(name)`으로 획득. +- `interrupt fn name()` — 모든 레지스터 보존 + `iret`. 파라미터/반환 없음. 주소는 `@as_far_fn(name)`으로 획득. 호출 제한과 공유 상태 규칙은 R10을 따른다. +- `interrupt_safe fn name(...)` — 인터럽트 문맥에서 호출 가능한 함수. ABI는 일반 함수와 같고 R10의 제한을 본문 검사로 만족해야 한다. - 큰 struct(> 4바이트)는 숨은 포인터로 반환(C ABI 따름). ### 7.4 검사와 트랩 @@ -488,13 +530,15 @@ var xs: List(u8) = List(u8).new(); ## 10. 표준 라이브러리 (최소 집합) - **core**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. -- **mem**: `create(T) -> !^T`, `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `copy(dst, src)`, `set(dst, v)`, `Arena{ init, alloc, reset, drop }`. +- **mem**: `create(T) -> !^T`, `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst, src)`, `set(dst, v)`, `Arena{ init, alloc, reset, drop }`. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 재귀 구조의 반복 drop에도 사용한다. - **str**: `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr(buf, s)`, `from_cstr(p)`. - **list**: `List(T)`. - **map**: `Map(K, V)` (오픈 어드레싱, `K`는 정수/str). - **fmt**: `@print` 계열이 전개해 호출하는 저수준 함수 모음. `write_str(w, s)`, `write_int_i8/i16/i32/u8/u16/u32(w, v)`, `write_hex_u8/u16/u32(w, v)`, - `write_char(w, c)`, `write_bool(w, b)`, `write_int_pad(w, v, width, pad)`. + `write_char(w, c)`, `write_bool(w, b)`, `write_error(w, e)`, + `write_int_pad(w, v, width, pad)`. `write_error`는 `--strip-error-names` 설정을 + 따르며, 모든 함수는 `!void`를 반환한다. 전부 `fn(w: &mut io.Writer, ...) -> !void` 시그니처. 사용자가 직접 호출해도 된다. - **io**: - `File{ open, create, read, write, seek, size, close(=drop) }`, `stdin`, `stdout`, `stderr`. @@ -589,7 +633,9 @@ fec/ - **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. - **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. - **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. -- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. +- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. `@print`는 각 호출의 오류 코드를 명시적으로 버리고 `void`가 되며, `@fprint`는 첫 오류를 전파하고, `@sprint`는 남은 버퍼 길이를 추적해 잘라 쓴 뒤 실제 길이를 반환한다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. +- **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. +- **임계 구역**: bits16의 `critical`은 진입 시 FLAGS를 저장한 뒤 `cli`하고 모든 이탈 경로에서 저장한 FLAGS를 복원한다. `shared atomic var`의 단일 접근도 같은 보존형 시퀀스를 사용하며 무조건 `sti`하지 않는다. - **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. - 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성. @@ -619,13 +665,13 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive | M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 | | M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 | | M3 | struct, enum, match, 배열, 슬라이스, 경계 검사, str | 문자열 처리 예제 통과 | -| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), `io.Writer` 함수 포인터 struct | 포맷 출력 동작, 인자 개수·타입 불일치가 컴파일 에러 | +| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), `io.Writer` 함수 포인터 struct | `@print`의 `void` 오류 삼킴, `@fprint`의 `!void` 전파, `@sprint`의 잘림/길이 동작과 인자 개수·타입 불일치가 컴파일 에러 | | M5 | `^T`, drop, defer, 이동 검사 | 누수/이중해제 테스트 통과 | | M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | | M7 | `?T`, `E!T`, try/catch | io 유닛 동작 | | M8 | 유닛/import/.fei, 분리 컴파일, std 초안 | 다중 유닛 프로그램 빌드 | | M9 | **제네릭** (모노모피제이션) | `List(T)`, `Map(K,V)`를 Ferro로 재작성 | -| M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn | DOSBox에서 VGA 데모 실행 | +| M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn, `shared`/`atomic`/`critical`/`interrupt_safe` | QEMU FreeDOS에서 자동화된 far 포인터·인터럽트 공유 상태 테스트 통과(VGA 데모는 수동/멀티모달 검증 대상이라 완료 게이트에서 제외) | | M11 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M10 테스트 통과 | | M12 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | | M13 | 386 네이티브 백엔드 | gcc 없이 빌드, 컴파일 속도 10배 | @@ -644,13 +690,15 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive tests/ pass/*.fe + *.expected 컴파일→실행→stdout 비교 fail/*.fe 첫 줄 "// ERROR::<메시지 일부>" - run16/*.fe bits16 빌드 후 DOSBox 실행, 출력 파일 비교 + run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교 boot/ A/B 출력 비교, fixpoint 검증 ``` - `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조 반환 바인딩), R9(unsafe 밖 raw 역참조), match 완전성, 암묵 변환, 타입 불일치. -- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`. -- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@sprint` 반환 길이 검증, `@fprint`를 `io.buf_writer`로 호출. +- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`, `try @print(...)`(void). +- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print`의 오류 삼킴, `@sprint` 반환 길이/잘림 검증, `@fprint`를 `io.buf_writer`로 호출. +- `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 + `core.Error` 등록, 결정적 코드, `--strip-error-names`, `fmt.write_error`를 각각 pass/fail로 검증한다. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. - 회귀 실행: `make test` — 전 타깃 전 테스트. @@ -673,8 +721,9 @@ tests/ | 레이블 있는 break | 편의 | — | 플래그 변수 | | 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | | `inline fn` | 편의 | — | C 방출 시 `static inline` | +| `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | | 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), 인덱스 핸들 | -| 스레드 | 구조적 불가 | DOS에 없음 | — | +| 선점형 스레드 | 구조적 불가 | DOS 기본 실행 모델에 없고 함수 단위 소유권 모델을 넘어서는 동기화가 필요 | — | | 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` | | 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 | | GC | 영구 | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 | From 005056a5eaaa8c46b0529710141737698c6c00a5 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 07:10:53 +0900 Subject: [PATCH 004/184] feat: implement M1 Ferro frontend --- .gitignore | 1 + fec/Makefile | 23 +++ fec/build-dos.bat | 29 ++++ fec/src/arena.c | 56 +++++++ fec/src/arena.h | 17 ++ fec/src/ast.c | 39 +++++ fec/src/ast.h | 41 +++++ fec/src/diag.c | 20 +++ fec/src/diag.h | 21 +++ fec/src/driver.c | 25 +++ fec/src/lexer.c | 170 ++++++++++++++++++++ fec/src/lexer.h | 50 ++++++ fec/src/parser.c | 201 ++++++++++++++++++++++++ fec/src/parser.h | 17 ++ fec/std/core.fe | 5 + fec/std/fmt.fe | 4 + fec/std/io.fe | 9 ++ fec/std/list.fe | 6 + fec/std/map.fe | 4 + fec/std/mem.fe | 11 ++ fec/std/str.fe | 3 + fec/std/sys.fe | 2 + fec/test-dos.bat | 51 ++++++ fec/tests/fail/logical-symbols.fe | 3 + fec/tests/fail/missing-semi.fe | 3 + fec/tests/fail/unclosed-comment.fe | 3 + fec/tests/pass/basic.fe | 25 +++ fec/tests/pass/keywords-and-builtins.fe | 9 ++ fec/tests/pass/literals.fe | 12 ++ fec/tests/pass/v012-forms.fe | 20 +++ fec/tests/run-tests.sh | 20 +++ fec/vm-m1.bat | 86 ++++++++++ 32 files changed, 986 insertions(+) create mode 100644 fec/Makefile create mode 100644 fec/build-dos.bat create mode 100644 fec/src/arena.c create mode 100644 fec/src/arena.h create mode 100644 fec/src/ast.c create mode 100644 fec/src/ast.h create mode 100644 fec/src/diag.c create mode 100644 fec/src/diag.h create mode 100644 fec/src/driver.c create mode 100644 fec/src/lexer.c create mode 100644 fec/src/lexer.h create mode 100644 fec/src/parser.c create mode 100644 fec/src/parser.h create mode 100644 fec/std/core.fe create mode 100644 fec/std/fmt.fe create mode 100644 fec/std/io.fe create mode 100644 fec/std/list.fe create mode 100644 fec/std/map.fe create mode 100644 fec/std/mem.fe create mode 100644 fec/std/str.fe create mode 100644 fec/std/sys.fe create mode 100644 fec/test-dos.bat create mode 100644 fec/tests/fail/logical-symbols.fe create mode 100644 fec/tests/fail/missing-semi.fe create mode 100644 fec/tests/fail/unclosed-comment.fe create mode 100644 fec/tests/pass/basic.fe create mode 100644 fec/tests/pass/keywords-and-builtins.fe create mode 100644 fec/tests/pass/literals.fe create mode 100644 fec/tests/pass/v012-forms.fe create mode 100644 fec/tests/run-tests.sh create mode 100644 fec/vm-m1.bat diff --git a/.gitignore b/.gitignore index 4920e87..c9ce8fb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ !.qemu/*.txt !.qemu/share/ !.qemu/share/*.c +.qemu/share/fec/ .qemu/*.qcow2 .qemu/*.img diff --git a/fec/Makefile b/fec/Makefile new file mode 100644 index 0000000..0891f8f --- /dev/null +++ b/fec/Makefile @@ -0,0 +1,23 @@ +CC ?= cc +CFLAGS ?= -O2 -Wall -Wextra -std=c89 +CPPFLAGS ?= -Isrc +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/driver.c +OBJ = $(SRC:.c=.o) + +.PHONY: all clean test dos-build +all: fec + +fec: $(OBJ) + $(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $(OBJ) + +src/%.o: src/%.c + $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< + +test: fec + @./tests/run-tests.sh + +dos-build: + @echo "Run build-dos.bat inside FreeDOS/Open Watcom." + +clean: + $(RM) $(OBJ) fec diff --git a/fec/build-dos.bat b/fec/build-dos.bat new file mode 100644 index 0000000..510bbfa --- /dev/null +++ b/fec/build-dos.bat @@ -0,0 +1,29 @@ +@echo off +rem Open Watcom C89 build. TEST-DOS.BAT runs this from C:\FEC. +C: +cd \FEC +if exist BUILD.OK del BUILD.OK +if exist BUILD.FAIL del BUILD.FAIL +if exist fec.exe del fec.exe +if exist __wcl__.lnk del __wcl__.lnk +if exist arena.obj del arena.obj +if exist diag.obj del diag.obj +if exist lexer.obj del lexer.obj +if exist ast.obj del ast.obj +if exist parser.obj del parser.obj +if exist driver.obj del driver.obj + +if "%WATCOM%"=="" set WATCOM=C:\DEVEL\WATCOMC +if not exist %WATCOM%\BINW\WCL.EXE goto build_fail +set PATH=%WATCOM%\BINW;%WATCOM%\BINP;%PATH% +wcl -q -za -wx -bt=dos -k32768 -fe=fec.exe src\arena.c src\diag.c src\lexer.c src\ast.c src\parser.c src\driver.c +if errorlevel 1 goto build_fail +if not exist fec.exe goto build_fail +echo OK>BUILD.OK +cd C:\FEC +goto build_done + +:build_fail +echo FAIL>BUILD.FAIL + +:build_done diff --git a/fec/src/arena.c b/fec/src/arena.c new file mode 100644 index 0000000..b7a0bf2 --- /dev/null +++ b/fec/src/arena.c @@ -0,0 +1,56 @@ +#include "arena.h" +#include +#include + +struct FeArenaBlock { + FeArenaBlock *next; + size_t used; + size_t size; + unsigned char data[1]; +}; + +void fe_arena_init(FeArena *a, size_t block_size) +{ + a->blocks = 0; + a->block_size = block_size ? block_size : 16384; +} + +void fe_arena_destroy(FeArena *a) +{ + FeArenaBlock *b = a->blocks; + while (b) { + FeArenaBlock *n = b->next; + free(b); + b = n; + } + a->blocks = 0; +} + +void *fe_arena_alloc(FeArena *a, size_t size) +{ + FeArenaBlock *b; + size_t need; + if (size == 0) size = 1; + need = (size + 7u) & ~(size_t)7u; + b = a->blocks; + if (!b || b->used + need > b->size) { + size_t bs = a->block_size > need ? a->block_size : need; + b = (FeArenaBlock *)malloc(sizeof(FeArenaBlock) + bs - 1); + if (!b) return 0; + b->next = a->blocks; + b->used = 0; + b->size = bs; + a->blocks = b; + } + b->used += need; + return b->data + b->used - need; +} + +char *fe_arena_strdup(FeArena *a, const char *s, size_t n) +{ + char *p = (char *)fe_arena_alloc(a, n + 1); + if (!p) return 0; + if (n) memcpy(p, s, n); + p[n] = '\0'; + return p; +} diff --git a/fec/src/arena.h b/fec/src/arena.h new file mode 100644 index 0000000..8e175f4 --- /dev/null +++ b/fec/src/arena.h @@ -0,0 +1,17 @@ +#ifndef FE_ARENA_H +#define FE_ARENA_H + +#include + +typedef struct FeArenaBlock FeArenaBlock; +typedef struct FeArena { + FeArenaBlock *blocks; + size_t block_size; +} FeArena; + +void fe_arena_init(FeArena *a, size_t block_size); +void fe_arena_destroy(FeArena *a); +void *fe_arena_alloc(FeArena *a, size_t size); +char *fe_arena_strdup(FeArena *a, const char *s, size_t n); + +#endif diff --git a/fec/src/ast.c b/fec/src/ast.c new file mode 100644 index 0000000..81b0e14 --- /dev/null +++ b/fec/src/ast.c @@ -0,0 +1,39 @@ +#include "ast.h" +#include + +void fe_ast_init(FeAst *a) { fe_arena_init(&a->arena, 32768); a->root=0; } +void fe_ast_destroy(FeAst *a) { fe_arena_destroy(&a->arena); a->root=0; } +FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned long len) +{ + FeNode *n=(FeNode *)fe_arena_alloc(&a->arena,sizeof(FeNode)); + if (!n) return 0; + n->kind=k; n->loc=loc; n->text=text?fe_arena_strdup(&a->arena,text,len):0; + n->a=n->b=n->c=n->children=n->next=0; return n; +} +void fe_node_add(FeNode *parent, FeNode *child) +{ + FeNode *p; + if (!child) return; + if (!parent->children) { parent->children=child; return; } + p=parent->children; while(p->next) p=p->next; p->next=child; +} +static void spaces(int n, FILE *out) { while(n-->0) fputc(' ',out); } +void fe_ast_dump(const FeNode *n, int indent, FILE *out) +{ + const FeNode *c; + if (!n) return; + spaces(indent,out); fprintf(out,"(%s",fe_node_name(n->kind)); + if (n->text) fprintf(out," %s",n->text); + fputc('\n',out); + if (n->a) fe_ast_dump(n->a,indent+2,out); + if (n->b) fe_ast_dump(n->b,indent+2,out); + if (n->c) fe_ast_dump(n->c,indent+2,out); + for(c=n->children;c;c=c->next) fe_ast_dump(c,indent+2,out); + spaces(indent,out); fputc(')',out); fputc('\n',out); +} +const char *fe_node_name(FeNodeKind k) +{ + static const char *names[] = {"unit","import","fn","struct","enum","error","const","global","field","param","variant","block","let","var","expr-stmt","assign","if","while","for","match","arm","return","break","continue","defer","unsafe","asm","type","expr","binary","unary","call","index","member","literal","ident","struct-init","error"}; + if ((unsigned)k >= sizeof(names)/sizeof(names[0])) return "node"; + return names[k]; +} diff --git a/fec/src/ast.h b/fec/src/ast.h new file mode 100644 index 0000000..3b26b02 --- /dev/null +++ b/fec/src/ast.h @@ -0,0 +1,41 @@ +#ifndef FE_AST_H +#define FE_AST_H + +#include "arena.h" +#include "lexer.h" +#include + +typedef enum FeNodeKind { + FE_N_UNIT, FE_N_IMPORT, FE_N_FN, FE_N_STRUCT, FE_N_ENUM, FE_N_ERROR_DECL, FE_N_CONST, + FE_N_GLOBAL, FE_N_FIELD, FE_N_PARAM, FE_N_VARIANT, FE_N_BLOCK, FE_N_LET, FE_N_VAR, + FE_N_EXPR_STMT, FE_N_ASSIGN, FE_N_IF, FE_N_WHILE, FE_N_FOR, FE_N_MATCH, FE_N_ARM, + FE_N_RETURN, FE_N_BREAK, FE_N_CONTINUE, FE_N_DEFER, FE_N_UNSAFE, FE_N_ASM, + FE_N_TYPE, FE_N_EXPR, FE_N_BINARY, FE_N_UNARY, FE_N_CALL, FE_N_INDEX, FE_N_MEMBER, + FE_N_LITERAL, FE_N_IDENT, FE_N_STRUCT_INIT, FE_N_ERROR_NODE +} FeNodeKind; + +typedef struct FeNode FeNode; +struct FeNode { + FeNodeKind kind; + FeLoc loc; + char *text; + FeNode *a; + FeNode *b; + FeNode *c; + FeNode *children; + FeNode *next; +}; + +typedef struct FeAst { + FeArena arena; + FeNode *root; +} FeAst; + +void fe_ast_init(FeAst *a); +void fe_ast_destroy(FeAst *a); +FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned long len); +void fe_node_add(FeNode *parent, FeNode *child); +void fe_ast_dump(const FeNode *n, int indent, FILE *out); +const char *fe_node_name(FeNodeKind k); + +#endif diff --git a/fec/src/diag.c b/fec/src/diag.c new file mode 100644 index 0000000..21612ce --- /dev/null +++ b/fec/src/diag.c @@ -0,0 +1,20 @@ +#include "diag.h" + +void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg) +{ + d->errors++; + fprintf(stderr, "%s:%lu:%lu: error: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); +} + +void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg) +{ + d->errors++; + fprintf(stderr, "%s:%lu:%lu: error: ", loc.file ? loc.file : "", loc.line, loc.col); + fprintf(stderr, msg, arg); + fputc('\n', stderr); +} + +void fe_diag_note(FeLoc loc, const char *msg) +{ + fprintf(stderr, "%s:%lu:%lu: note: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); +} diff --git a/fec/src/diag.h b/fec/src/diag.h new file mode 100644 index 0000000..6211e84 --- /dev/null +++ b/fec/src/diag.h @@ -0,0 +1,21 @@ +#ifndef FE_DIAG_H +#define FE_DIAG_H + +#include + +typedef struct FeLoc { + const char *file; + unsigned long line; + unsigned long col; +} FeLoc; + +typedef struct FeDiags { + unsigned long errors; + unsigned long warnings; +} FeDiags; + +void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg); +void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg); +void fe_diag_note(FeLoc loc, const char *msg); + +#endif diff --git a/fec/src/driver.c b/fec/src/driver.c new file mode 100644 index 0000000..5c52c99 --- /dev/null +++ b/fec/src/driver.c @@ -0,0 +1,25 @@ +#include "parser.h" +#include +#include +#include + +static char *read_file(const char *name, unsigned long *size) +{ + FILE *f; long n; char *p; + f=fopen(name,"rb"); if(!f){fprintf(stderr,"fec: cannot open %s\n",name);return 0;} + if(fseek(f,0L,SEEK_END)!=0){fclose(f);return 0;} n=ftell(f); if(n<0){fclose(f);return 0;} rewind(f); + p=(char *)malloc((unsigned long)n+1); if(!p){fclose(f);return 0;} + if(n && fread(p,1,(size_t)n,f)!=(size_t)n){free(p);fclose(f);return 0;} fclose(f);p[n]='\0';*size=(unsigned long)n;return p; +} +static void usage(void) +{ puts("usage: fec [--dump-ast] file.fe [--target=bits16|bits32] [--model=small|large]"); } +int main(int argc, char **argv) +{ + int i,dump=0; const char *file=0; unsigned long n; char *src; FeDiags d; FeAst ast; FeParser p; + if(argc<2){usage();return 2;} + for(i=1;i +#include + +typedef struct FeKw { const char *s; FeTokKind k; } FeKw; +static const FeKw keywords[] = { + {"unit",FE_TOK_UNIT},{"import",FE_TOK_IMPORT},{"pub",FE_TOK_PUB},{"fn",FE_TOK_FN}, + {"struct",FE_TOK_STRUCT},{"enum",FE_TOK_ENUM},{"error",FE_TOK_ERROR_KW},{"const",FE_TOK_CONST}, + {"static",FE_TOK_STATIC},{"var",FE_TOK_VAR},{"let",FE_TOK_LET},{"mut",FE_TOK_MUT}, + {"if",FE_TOK_IF},{"else",FE_TOK_ELSE},{"while",FE_TOK_WHILE},{"for",FE_TOK_FOR},{"in",FE_TOK_IN}, + {"match",FE_TOK_MATCH},{"return",FE_TOK_RETURN},{"break",FE_TOK_BREAK},{"continue",FE_TOK_CONTINUE}, + {"defer",FE_TOK_DEFER},{"unsafe",FE_TOK_UNSAFE},{"comptime",FE_TOK_COMPTIME},{"asm",FE_TOK_ASM}, + {"try",FE_TOK_TRY},{"catch",FE_TOK_CATCH},{"as",FE_TOK_AS},{"extern",FE_TOK_EXTERN}, + {"interrupt",FE_TOK_INTERRUPT},{"interrupt_safe",FE_TOK_INTERRUPT_SAFE},{"far",FE_TOK_FAR}, + {"true",FE_TOK_TRUE},{"false",FE_TOK_FALSE},{"null",FE_TOK_NULL},{"undefined",FE_TOK_UNDEFINED}, + {"shared",FE_TOK_SHARED},{"atomic",FE_TOK_ATOMIC},{"critical",FE_TOK_CRITICAL}, + {"self",FE_TOK_SELF},{"Self",FE_TOK_SELFTYPE},{"type",FE_TOK_TYPE}, + {"packed",FE_TOK_PACKED},{"orelse",FE_TOK_ORELSE},{"and",FE_TOK_AND_KW},{"or",FE_TOK_OR_KW},{"not",FE_TOK_NOT}, + {0,FE_TOK_UNKNOWN} +}; + +static int at(FeLexer *l, unsigned long n, char c) { return l->pos + n < l->length && l->src[l->pos+n] == c; } +static FeLoc here(FeLexer *l, unsigned long line, unsigned long col) +{ FeLoc x; x.file=l->file; x.line=line; x.col=col; return x; } +static char cur(FeLexer *l) { return l->pos < l->length ? l->src[l->pos] : '\0'; } +static void advance(FeLexer *l) +{ + if (l->pos >= l->length) return; + if (l->src[l->pos] == '\n') { l->line++; l->col = 1; } + else l->col++; + l->pos++; +} +static void skip_space(FeLexer *l) +{ + for (;;) { + while (isspace((unsigned char)cur(l))) advance(l); + if (at(l,0,'/') && at(l,1,'/')) { + while (cur(l) && cur(l) != '\n') advance(l); + continue; + } + if (at(l,0,'/') && at(l,1,'*')) { + unsigned long depth = 0; + advance(l); advance(l); depth = 1; + while (depth && cur(l)) { + if (at(l,0,'/') && at(l,1,'*')) { advance(l); advance(l); depth++; } + else if (at(l,0,'*') && at(l,1,'/')) { advance(l); advance(l); depth--; } + else advance(l); + } + if (depth) fe_diag_error(l->diags, here(l,l->line,l->col), "unterminated block comment"); + continue; + } + break; + } +} + +void fe_lexer_init(FeLexer *l, const char *src, unsigned long length, const char *file, FeDiags *d) +{ + l->src=src; l->length=length; l->pos=0; l->line=1; l->col=1; l->file=file; l->diags=d; +} + +static FeTokKind keyword(const char *s, unsigned long n) +{ + unsigned long i; + for (i=0; keywords[i].s; i++) { + if (strlen(keywords[i].s)==n && memcmp(keywords[i].s,s,n)==0) return keywords[i].k; + } + return FE_TOK_IDENT; +} +static FeToken tok(FeLexer *l, FeTokKind k, unsigned long start, unsigned long line, unsigned long col) +{ + FeToken t; t.kind=k; t.begin=l->src+start; t.length=l->pos-start; t.loc.file=l->file; t.loc.line=line; t.loc.col=col; return t; +} +static int digit_for_base(char c, int base) +{ + int d; + if (c >= '0' && c <= '9') d=c-'0'; + else if (c >= 'a' && c <= 'f') d=c-'a'+10; + else if (c >= 'A' && c <= 'F') d=c-'A'+10; + else return 0; + return d < base; +} + +FeToken fe_lexer_next(FeLexer *l) +{ + unsigned long start, line, col; + char c; + skip_space(l); + start=l->pos; line=l->line; col=l->col; c=cur(l); + if (!c) return tok(l,FE_TOK_EOF,start,line,col); + if (isalpha((unsigned char)c) || c=='_') { + advance(l); + while (isalnum((unsigned char)cur(l)) || cur(l)=='_') advance(l); + return tok(l,keyword(l->src+start,l->pos-start),start,line,col); + } + if (isdigit((unsigned char)c)) { + int base=10, had_digit=0; + if (c=='0' && (at(l,1,'x') || at(l,1,'X'))) { advance(l); advance(l); base=16; } + else if (c=='0' && (at(l,1,'b') || at(l,1,'B'))) { advance(l); advance(l); base=2; } + else if (c=='0' && (at(l,1,'o') || at(l,1,'O'))) { advance(l); advance(l); base=8; } + while (cur(l)=='_' || digit_for_base(cur(l),base)) { if(cur(l)!='_') had_digit=1; advance(l); } + if (!had_digit) fe_diag_error(l->diags,here(l,line,col),"integer literal has no digits"); + if (isalnum((unsigned char)cur(l))) { + fe_diag_error(l->diags,here(l,line,col),"invalid digit in integer literal"); + while (isalnum((unsigned char)cur(l)) || cur(l)=='_') advance(l); + } + return tok(l,FE_TOK_INT,start,line,col); + } + if (c=='\'' || c=='"') { + char quote=c; int bad=0, units=0; advance(l); + while (cur(l) && cur(l)!=quote) { + if (cur(l)=='\n' || cur(l)=='\r') { bad=1; break; } + units++; + if (cur(l)=='\\') { + advance(l); + if (!cur(l)) { bad=1; break; } + if (cur(l)=='x') { int i; advance(l); for(i=0;i<2;i++) { if(!digit_for_base(cur(l),16)) bad=1; else advance(l); } } + else if (cur(l)=='u') { int i; advance(l); for(i=0;i<4;i++) { if(!digit_for_base(cur(l),16)) bad=1; else advance(l); } } + else if (strchr("nrt\\'\"0",cur(l))) advance(l); + else { bad=1; advance(l); } + } else advance(l); + } + if (cur(l)==quote) advance(l); else bad=1; + if (quote=='\'' && units != 1) bad=1; + if (bad) fe_diag_error(l->diags,here(l,line,col),quote=='\''?"invalid character literal":"unterminated or invalid string literal"); + return tok(l,quote=='\''?FE_TOK_CHAR:FE_TOK_STRING,start,line,col); + } + advance(l); + switch(c) { + case '(': return tok(l,FE_TOK_LPAREN,start,line,col); case ')': return tok(l,FE_TOK_RPAREN,start,line,col); + case '{': return tok(l,FE_TOK_LBRACE,start,line,col); case '}': return tok(l,FE_TOK_RBRACE,start,line,col); + case '[': return tok(l,FE_TOK_LBRACKET,start,line,col); case ']': return tok(l,FE_TOK_RBRACKET,start,line,col); + case ',': return tok(l,FE_TOK_COMMA,start,line,col); case ';': return tok(l,FE_TOK_SEMI,start,line,col); + case ':': return tok(l,FE_TOK_COLON,start,line,col); case '@': return tok(l,FE_TOK_AT,start,line,col); + case '?': return tok(l,FE_TOK_QUESTION,start,line,col); + case '.': if (cur(l)=='.') { advance(l); return tok(l,FE_TOK_DOTDOT,start,line,col); } return tok(l,FE_TOK_DOT,start,line,col); + case '+': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_PLUS_EQ,start,line,col);} if(cur(l)=='%'){advance(l);return tok(l,FE_TOK_PLUS_WRAP,start,line,col);} return tok(l,FE_TOK_PLUS,start,line,col); + case '-': if(cur(l)=='>'){advance(l);return tok(l,FE_TOK_ARROW,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_MINUS_EQ,start,line,col);} if(cur(l)=='%'){advance(l);return tok(l,FE_TOK_MINUS_WRAP,start,line,col);} return tok(l,FE_TOK_MINUS,start,line,col); + case '*': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_STAR_EQ,start,line,col);} if(cur(l)=='%'){advance(l);return tok(l,FE_TOK_STAR_WRAP,start,line,col);} return tok(l,FE_TOK_STAR,start,line,col); + case '/': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_SLASH_EQ,start,line,col);} return tok(l,FE_TOK_SLASH,start,line,col); + case '%': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_PERCENT_EQ,start,line,col);} return tok(l,FE_TOK_PERCENT,start,line,col); + case '=': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_EQEQ,start,line,col);} if(cur(l)=='>'){advance(l);return tok(l,FE_TOK_FATARROW,start,line,col);} return tok(l,FE_TOK_EQ,start,line,col); + case '!': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_NE,start,line,col);} return tok(l,FE_TOK_BANG,start,line,col); + case '<': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_LE,start,line,col);} if(cur(l)=='<'){advance(l);if(cur(l)=='='){advance(l);return tok(l,FE_TOK_SHL_EQ,start,line,col);}return tok(l,FE_TOK_SHL,start,line,col);} return tok(l,FE_TOK_LT,start,line,col); + case '>': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_GE,start,line,col);} if(cur(l)=='>'){advance(l);if(cur(l)=='='){advance(l);return tok(l,FE_TOK_SHR_EQ,start,line,col);}return tok(l,FE_TOK_SHR,start,line,col);} return tok(l,FE_TOK_GT,start,line,col); + case '&': if(cur(l)=='&'){advance(l);fe_diag_error(l->diags,here(l,line,col),"&& is not a Ferro logical operator; use 'and'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_AND_EQ,start,line,col);} return tok(l,FE_TOK_AND,start,line,col); + case '|': if(cur(l)=='|'){advance(l);fe_diag_error(l->diags,here(l,line,col),"|| is not a Ferro logical operator; use 'or'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_OR_EQ,start,line,col);} return tok(l,FE_TOK_OR,start,line,col); + case '^': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_XOR_EQ,start,line,col);} return tok(l,FE_TOK_XOR,start,line,col); + default: fe_diag_error(l->diags,here(l,line,col),"unknown character"); return tok(l,FE_TOK_UNKNOWN,start,line,col); + } +} + +const char *fe_token_name(FeTokKind k) +{ + switch(k) { + case FE_TOK_EOF:return "eof"; case FE_TOK_IDENT:return "identifier"; case FE_TOK_INT:return "integer"; + case FE_TOK_CHAR:return "character"; case FE_TOK_STRING:return "string"; case FE_TOK_UNIT:return "unit"; + case FE_TOK_FN:return "fn"; case FE_TOK_STRUCT:return "struct"; case FE_TOK_ENUM:return "enum"; + case FE_TOK_ERROR_KW:return "error"; case FE_TOK_CONST:return "const"; case FE_TOK_LET:return "let"; + case FE_TOK_VAR:return "var"; case FE_TOK_IF:return "if"; case FE_TOK_ELSE:return "else"; + case FE_TOK_WHILE:return "while"; case FE_TOK_FOR:return "for"; case FE_TOK_MATCH:return "match"; + case FE_TOK_RETURN:return "return"; case FE_TOK_BREAK:return "break"; case FE_TOK_CONTINUE:return "continue"; + case FE_TOK_TRUE:return "true"; case FE_TOK_FALSE:return "false"; case FE_TOK_NULL:return "null"; + case FE_TOK_UNDEFINED:return "undefined"; case FE_TOK_AND_KW:return "and"; case FE_TOK_OR_KW:return "or"; + case FE_TOK_NOT:return "not"; case FE_TOK_BANG:return "!"; + case FE_TOK_LBRACE:return "{"; case FE_TOK_RBRACE:return "}"; case FE_TOK_LPAREN:return "("; case FE_TOK_RPAREN:return ")"; + case FE_TOK_SEMI:return ";"; case FE_TOK_COLON:return ":"; case FE_TOK_COMMA:return ","; + case FE_TOK_EQ:return "="; case FE_TOK_ARROW:return "->"; case FE_TOK_FATARROW:return "=>"; + default:return "token"; + } +} diff --git a/fec/src/lexer.h b/fec/src/lexer.h new file mode 100644 index 0000000..7b1cda7 --- /dev/null +++ b/fec/src/lexer.h @@ -0,0 +1,50 @@ +#ifndef FE_LEXER_H +#define FE_LEXER_H + +#include "diag.h" +#include "arena.h" + +typedef enum FeTokKind { + FE_TOK_EOF, FE_TOK_ERROR, FE_TOK_IDENT, FE_TOK_INT, FE_TOK_CHAR, FE_TOK_STRING, + FE_TOK_UNIT, FE_TOK_IMPORT, FE_TOK_PUB, FE_TOK_FN, FE_TOK_STRUCT, FE_TOK_ENUM, + FE_TOK_ERROR_KW, FE_TOK_CONST, FE_TOK_STATIC, FE_TOK_VAR, FE_TOK_LET, FE_TOK_MUT, + FE_TOK_IF, FE_TOK_ELSE, FE_TOK_WHILE, FE_TOK_FOR, FE_TOK_IN, FE_TOK_MATCH, + FE_TOK_RETURN, FE_TOK_BREAK, FE_TOK_CONTINUE, FE_TOK_DEFER, FE_TOK_UNSAFE, + FE_TOK_COMPTIME, FE_TOK_ASM, FE_TOK_TRY, FE_TOK_CATCH, FE_TOK_AS, FE_TOK_EXTERN, + FE_TOK_INTERRUPT, FE_TOK_INTERRUPT_SAFE, FE_TOK_FAR, FE_TOK_TRUE, FE_TOK_FALSE, FE_TOK_NULL, + FE_TOK_UNDEFINED, FE_TOK_SHARED, FE_TOK_ATOMIC, FE_TOK_CRITICAL, FE_TOK_SELF, + FE_TOK_SELFTYPE, FE_TOK_TYPE, FE_TOK_PACKED, FE_TOK_ORELSE, + FE_TOK_LPAREN, FE_TOK_RPAREN, FE_TOK_LBRACE, FE_TOK_RBRACE, FE_TOK_LBRACKET, FE_TOK_RBRACKET, + FE_TOK_COMMA, FE_TOK_SEMI, FE_TOK_COLON, FE_TOK_DOT, FE_TOK_DOTDOT, + FE_TOK_PLUS, FE_TOK_MINUS, FE_TOK_STAR, FE_TOK_SLASH, FE_TOK_PERCENT, + FE_TOK_PLUS_EQ, FE_TOK_MINUS_EQ, FE_TOK_STAR_EQ, FE_TOK_SLASH_EQ, FE_TOK_PERCENT_EQ, + FE_TOK_PLUS_WRAP, FE_TOK_MINUS_WRAP, FE_TOK_STAR_WRAP, + FE_TOK_EQ, FE_TOK_EQEQ, FE_TOK_NE, FE_TOK_LT, FE_TOK_LE, FE_TOK_GT, FE_TOK_GE, + FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR, + FE_TOK_AND_EQ, FE_TOK_OR_EQ, FE_TOK_XOR_EQ, FE_TOK_SHL_EQ, FE_TOK_SHR_EQ, + FE_TOK_ANDAND, FE_TOK_OROR, FE_TOK_ARROW, FE_TOK_FATARROW, FE_TOK_AT, + FE_TOK_QUESTION, FE_TOK_UNKNOWN +} FeTokKind; + +typedef struct FeToken { + FeTokKind kind; + const char *begin; + unsigned long length; + FeLoc loc; +} FeToken; + +typedef struct FeLexer { + const char *src; + unsigned long length; + unsigned long pos; + unsigned long line; + unsigned long col; + const char *file; + FeDiags *diags; +} FeLexer; + +void fe_lexer_init(FeLexer *l, const char *src, unsigned long length, const char *file, FeDiags *d); +FeToken fe_lexer_next(FeLexer *l); +const char *fe_token_name(FeTokKind k); + +#endif diff --git a/fec/src/parser.c b/fec/src/parser.c new file mode 100644 index 0000000..4efa04d --- /dev/null +++ b/fec/src/parser.c @@ -0,0 +1,201 @@ +#include "parser.h" +#include +#include + +static FeToken next(FeParser *p) { p->previous=p->current; p->current=fe_lexer_next(&p->lexer); return p->current; } +static int is(FeParser *p, FeTokKind k) { return p->current.kind==k; } +static int eat(FeParser *p, FeTokKind k) { if(is(p,k)){next(p);return 1;}return 0; } +static FeNode *toknode(FeParser *p, FeNodeKind k, FeToken t) { return fe_node(p->ast,k,t.loc,t.begin,t.length); } +static void error(FeParser *p, const char *s) { fe_diag_error(p->diags,p->current.loc,s); } +static int want(FeParser *p, FeTokKind k, const char *what) +{ if(eat(p,k)) return 1; error(p,what); return 0; } +static int is_name(FeParser *p) { return is(p,FE_TOK_IDENT)||is(p,FE_TOK_SELF)||is(p,FE_TOK_SELFTYPE); } +static FeNode *expr(FeParser *p, int minprec); +static FeNode *type(FeParser *p); +static FeNode *statement(FeParser *p); +static FeNode *block(FeParser *p); + +void fe_parser_init(FeParser *p, FeAst *ast, const char *src, unsigned long length, const char *file, FeDiags *d) +{ + p->ast=ast; p->diags=d; fe_lexer_init(&p->lexer,src,length,file,d); + p->previous=p->current=fe_lexer_next(&p->lexer); +} + +static void recover(FeParser *p) +{ + while(!is(p,FE_TOK_EOF) && !is(p,FE_TOK_SEMI) && !is(p,FE_TOK_RBRACE)) next(p); + if(is(p,FE_TOK_SEMI)) next(p); +} + +static FeNode *type_prefix(FeParser *p, FeTokKind k, FeToken op) +{ + FeNode *n; + (void)k; + n=toknode(p,FE_N_TYPE,op); n->a=type(p); return n; +} +static FeNode *type(FeParser *p) +{ + FeToken t=p->current; FeNode *n; + if (is(p,FE_TOK_QUESTION)||is(p,FE_TOK_BANG)||is(p,FE_TOK_STAR)||is(p,FE_TOK_XOR)) { + next(p); return type_prefix(p,t.kind,t); + } + if (is(p,FE_TOK_AND)) { + next(p); n=toknode(p,FE_N_TYPE,t); if(eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=type(p); return n; + } + if (is(p,FE_TOK_FAR)) { + next(p); n=toknode(p,FE_N_TYPE,t); if(is(p,FE_TOK_STAR)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)) next(p); n->a=type(p); return n; + } + if (is(p,FE_TOK_LBRACKET)) { + next(p); n=toknode(p,FE_N_TYPE,t); + if(!eat(p,FE_TOK_RBRACKET)) { n->a=expr(p,0); want(p,FE_TOK_RBRACKET,"expected ']' in array type"); } + n->b=type(p); return n; + } + if (is(p,FE_TOK_FN)) { + next(p); n=toknode(p,FE_N_TYPE,t); want(p,FE_TOK_LPAREN,"expected '(' in function type"); + while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { fe_node_add(n,type(p)); if(!eat(p,FE_TOK_COMMA)) break; } + want(p,FE_TOK_RPAREN,"expected ')' in function type"); if(eat(p,FE_TOK_ARROW)) n->a=type(p); return n; + } + if (is_name(p) || is(p,FE_TOK_TYPE)) { + next(p); n=toknode(p,FE_N_TYPE,t); + if (eat(p,FE_TOK_DOT)) { if(is_name(p)){FeNode *m=toknode(p,FE_N_IDENT,p->previous); n->a=m; next(p);} else error(p,"expected type name after '.'"); } + if (eat(p,FE_TOK_BANG)) { FeNode *e=toknode(p,FE_N_TYPE,p->previous); e->a=n; e->b=type(p); return e; } + if (eat(p,FE_TOK_LPAREN)) { while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' in generic type"); } + return n; + } + error(p,"expected type"); next(p); return fe_node(p->ast,FE_N_TYPE,t.loc,"error",5); +} + +static int precedence(FeTokKind k) +{ + switch(k) { + case FE_TOK_ORELSE: case FE_TOK_CATCH:return 1; + case FE_TOK_OR_KW:return 2; case FE_TOK_AND_KW:return 3; + case FE_TOK_EQEQ: case FE_TOK_NE: case FE_TOK_LT: case FE_TOK_LE: case FE_TOK_GT: case FE_TOK_GE:return 4; + case FE_TOK_OR:return 5; case FE_TOK_XOR:return 6; case FE_TOK_AND:return 7; + case FE_TOK_SHL: case FE_TOK_SHR:return 8; + case FE_TOK_PLUS: case FE_TOK_MINUS: case FE_TOK_PLUS_WRAP: case FE_TOK_MINUS_WRAP:return 9; + case FE_TOK_STAR: case FE_TOK_SLASH: case FE_TOK_PERCENT: case FE_TOK_STAR_WRAP:return 10; + default:return 0; + } +} +static FeNode *primary(FeParser *p) +{ + FeToken t=p->current; FeNode *n; + if(is(p,FE_TOK_INT)||is(p,FE_TOK_CHAR)||is(p,FE_TOK_STRING)||is(p,FE_TOK_TRUE)||is(p,FE_TOK_FALSE)||is(p,FE_TOK_NULL)||is(p,FE_TOK_UNDEFINED)) {next(p);return toknode(p,FE_N_LITERAL,t);} + if(is_name(p) || is(p,FE_TOK_ERROR_KW)) { + next(p); n=toknode(p,FE_N_IDENT,t); + if(is(p,FE_TOK_LBRACE)) { + FeNode *s=toknode(p,FE_N_STRUCT_INIT,t); next(p); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { FeNode *f; + if(!is_name(p)){error(p,"expected field name");recover(p);break;} f=toknode(p,FE_N_FIELD,p->current);next(p);want(p,FE_TOK_COLON,"expected ':' after field");f->a=expr(p,0);fe_node_add(s,f);if(!eat(p,FE_TOK_COMMA))break; + } want(p,FE_TOK_RBRACE,"expected '}' in struct literal"); return s; + } + return n; + } + if(eat(p,FE_TOK_LPAREN)) { n=expr(p,0); want(p,FE_TOK_RPAREN,"expected ')'"); return n; } + if(eat(p,FE_TOK_AT)) { + FeToken name=p->current; if(!is_name(p)){error(p,"expected builtin name after '@'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"builtin",7);} next(p); + n=toknode(p,FE_N_CALL,name); n->text=fe_arena_strdup(&p->ast->arena,name.begin-1,name.length+1); + if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,expr(p,0));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after builtin");} + return n; + } + error(p,"expected expression"); next(p); return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"expression",10); +} +static FeNode *postfix(FeParser *p) +{ + FeNode *n=primary(p); + for(;;) { + FeToken t=p->current; FeNode *m; + if(eat(p,FE_TOK_LPAREN)) { m=toknode(p,FE_N_CALL,t); m->a=n; while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(m,expr(p,0));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' after call"); n=m; } + else if(eat(p,FE_TOK_LBRACKET)) { m=toknode(p,FE_N_INDEX,t);m->a=n;m->b=expr(p,0);if(eat(p,FE_TOK_DOTDOT)){m->c=expr(p,0);}want(p,FE_TOK_RBRACKET,"expected ']' after index");n=m; } + else if(eat(p,FE_TOK_DOT)) { m=toknode(p,FE_N_MEMBER,t);m->a=n;if(is_name(p)){m->b=toknode(p,FE_N_IDENT,p->current);next(p);}else if(eat(p,FE_TOK_QUESTION)){m->text=fe_arena_strdup(&p->ast->arena,".?",2);}else error(p,"expected member name");n=m; } + else if(eat(p,FE_TOK_AS)) { m=toknode(p,FE_N_TYPE,t);m->a=n;m->b=type(p);n=m; } + else break; + } + return n; +} +static FeNode *expr(FeParser *p, int minprec) +{ + FeToken t=p->current; FeNode *left,*n; int prec; + if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); n->a=expr(p,11); left=n; } + else left=postfix(p); + for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break;next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; } + return left; +} + +static FeNode *params(FeParser *p) +{ + FeNode *list=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"params",6); + want(p,FE_TOK_LPAREN,"expected '(' after function name"); + while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { FeToken t=p->current; FeNode *q; + if(eat(p,FE_TOK_COMPTIME)) t=p->previous; + if(!is_name(p)){error(p,"expected parameter name");recover(p);break;} q=toknode(p,FE_N_PARAM,t);next(p);want(p,FE_TOK_COLON,"expected ':' in parameter");q->a=type(p);fe_node_add(list,q);if(!eat(p,FE_TOK_COMMA))break; + } + want(p,FE_TOK_RPAREN,"expected ')' after parameters"); return list; +} +static FeNode *fn_decl(FeParser *p, int pub, int external, int interrupt, int interrupt_safe) +{ + FeToken t=p->current, name; FeNode *n; + (void)pub; (void)external; (void)interrupt; (void)interrupt_safe; + want(p,FE_TOK_FN,"expected 'fn'"); if(!is_name(p)){error(p,"expected function name");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"fn",2);} + name=p->current; n=toknode(p,FE_N_FN,t); n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n; +} +static FeNode *field(FeParser *p) +{ + FeToken t=p->current; FeNode *n; + if(!is_name(p)){error(p,"expected field name");recover(p);return 0;} next(p);n=toknode(p,FE_N_FIELD,t);want(p,FE_TOK_COLON,"expected ':' after field");n->a=type(p);if(!eat(p,FE_TOK_COMMA) && !is(p,FE_TOK_RBRACE)) error(p,"expected ',' after field");return n; +} +static FeNode *decl(FeParser *p) +{ + int pub=0, external=0, interrupt=0, interrupt_safe=0, shared=0, atomic=0; FeToken t=p->current; FeNode *n; + (void)shared; (void)atomic; + if(eat(p,FE_TOK_PUB)) pub=1; + if(eat(p,FE_TOK_EXTERN)) { external=1; if(is(p,FE_TOK_STRING)) next(p); } + if(eat(p,FE_TOK_INTERRUPT)) interrupt=1; + if(eat(p,FE_TOK_INTERRUPT_SAFE)) interrupt_safe=1; + if(!is(p,FE_TOK_PACKED)) t=p->current; + if(is(p,FE_TOK_FN)) return fn_decl(p,pub,external,interrupt,interrupt_safe); + if(eat(p,FE_TOK_PACKED)) t=p->previous; + if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){if(is(p,FE_TOK_PUB))next(p);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,0,0,0,0));else fe_node_add(n,field(p));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } + if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } + if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } + if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } + if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } + error(p,"expected declaration"); recover(p); return 0; +} + +static FeNode *block(FeParser *p) +{ + FeToken t=p->current; FeNode *n=toknode(p,FE_N_BLOCK,t);want(p,FE_TOK_LBRACE,"expected '{'");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *s=statement(p);if(s)fe_node_add(n,s);}want(p,FE_TOK_RBRACE,"expected '}'");return n; +} +static FeNode *statement(FeParser *p) +{ + FeToken t=p->current; FeNode *n,*e; + if(is(p,FE_TOK_LBRACE)) return block(p); + if(eat(p,FE_TOK_LET)) { n=toknode(p,FE_N_LET,t);if(is_name(p))next(p);else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in let");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_VAR)) { n=toknode(p,FE_N_VAR,t);if(is_name(p))next(p);else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);if(eat(p,FE_TOK_EQ))n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_CONST)) { n=toknode(p,FE_N_CONST,t);if(is_name(p))next(p);else error(p,"expected constant name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in const");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_IF)) { n=toknode(p,FE_N_IF,t);if(eat(p,FE_TOK_LET)){n->text=fe_arena_strdup(&p->ast->arena,"if let",6);if(is_name(p))next(p);if(eat(p,FE_TOK_LPAREN)){if(is_name(p))next(p);want(p,FE_TOK_RPAREN,"expected ')' in if let pattern");}want(p,FE_TOK_EQ,"expected '=' in if let");}n->a=expr(p,0);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } + if(eat(p,FE_TOK_COMPTIME)) { n=toknode(p,FE_N_IF,t);want(p,FE_TOK_IF,"expected 'if' after comptime");n->text=fe_arena_strdup(&p->ast->arena,"comptime if",11);n->a=expr(p,0);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } + if(eat(p,FE_TOK_WHILE)) {n=toknode(p,FE_N_WHILE,t);n->a=expr(p,0);n->b=block(p);return n;} + if(eat(p,FE_TOK_FOR)) {n=toknode(p,FE_N_FOR,t);if(is_name(p))next(p);else error(p,"expected loop variable");if(eat(p,FE_TOK_COMMA)){if(is_name(p))next(p);else error(p,"expected second loop variable");}want(p,FE_TOK_IN,"expected 'in' in for");n->a=expr(p,0);if(eat(p,FE_TOK_DOTDOT))n->c=expr(p,0);n->b=block(p);return n;} + if(eat(p,FE_TOK_MATCH)) { n=toknode(p,FE_N_MATCH,t);n->a=expr(p,0);want(p,FE_TOK_LBRACE,"expected '{' after match expression");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *arm=toknode(p,FE_N_ARM,p->current);if(is_name(p)||is(p,FE_TOK_INT)||is(p,FE_TOK_CHAR)||is(p,FE_TOK_NULL)||is(p,FE_TOK_TRUE)||is(p,FE_TOK_FALSE)||is(p,FE_TOK_IDENT)){arm->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else{error(p,"expected match pattern");recover(p);continue;}while(is(p,FE_TOK_LPAREN)||is(p,FE_TOK_LBRACE)){FeTokKind close=is(p,FE_TOK_LPAREN)?FE_TOK_RPAREN:FE_TOK_RBRACE;next(p);while(!is(p,close)&&!is(p,FE_TOK_EOF))next(p);want(p,close,"expected end of match pattern");}want(p,FE_TOK_FATARROW,"expected '=>' in match arm");if(is(p,FE_TOK_LBRACE))arm->a=block(p);else{arm->a=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' in match arm");}fe_node_add(n,arm);}want(p,FE_TOK_RBRACE,"expected '}' after match");return n;} + if(eat(p,FE_TOK_RETURN)) {n=toknode(p,FE_N_RETURN,t);if(!is(p,FE_TOK_SEMI))n->a=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after return");return n;} + if(eat(p,FE_TOK_BREAK)){n=toknode(p,FE_N_BREAK,t);want(p,FE_TOK_SEMI,"expected ';'");return n;} + if(eat(p,FE_TOK_CONTINUE)){n=toknode(p,FE_N_CONTINUE,t);want(p,FE_TOK_SEMI,"expected ';'");return n;} + if(eat(p,FE_TOK_DEFER)){n=toknode(p,FE_N_DEFER,t);n->a=block(p);return n;} + if(eat(p,FE_TOK_UNSAFE)){n=toknode(p,FE_N_UNSAFE,t);n->a=block(p);return n;} + if(eat(p,FE_TOK_CRITICAL)){n=toknode(p,FE_N_UNSAFE,t);n->text=fe_arena_strdup(&p->ast->arena,"critical",8);n->a=block(p);return n;} + if(eat(p,FE_TOK_ASM)){n=toknode(p,FE_N_ASM,t);want(p,FE_TOK_LBRACE,"expected '{' after asm");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))next(p);want(p,FE_TOK_RBRACE,"expected '}' after asm");return n;} + e=expr(p,0); if(is(p,FE_TOK_EQ)||is(p,FE_TOK_PLUS_EQ)||is(p,FE_TOK_MINUS_EQ)||is(p,FE_TOK_STAR_EQ)||is(p,FE_TOK_SLASH_EQ)||is(p,FE_TOK_PERCENT_EQ)||is(p,FE_TOK_AND_EQ)||is(p,FE_TOK_OR_EQ)||is(p,FE_TOK_XOR_EQ)||is(p,FE_TOK_SHL_EQ)||is(p,FE_TOK_SHR_EQ)){n=toknode(p,FE_N_ASSIGN,p->current);n->a=e;next(p);n->b=expr(p,0);}else{n=toknode(p,FE_N_EXPR_STMT,t);n->a=e;}want(p,FE_TOK_SEMI,"expected ';' after statement");return n; +} + +FeNode *fe_parse_unit(FeParser *p) +{ + FeToken t=p->current, name; FeNode *root; + if(!eat(p,FE_TOK_UNIT)){error(p,"source must start with 'unit'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"unit",4);} + root=toknode(p,FE_N_UNIT,t);if(is_name(p)){name=p->current;root->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length);next(p);}else error(p,"expected unit name");want(p,FE_TOK_SEMI,"expected ';' after unit name"); + while(eat(p,FE_TOK_IMPORT)){FeToken it=p->previous;FeNode *i=toknode(p,FE_N_IMPORT,it);if(is_name(p)){next(p);i->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected import name");want(p,FE_TOK_SEMI,"expected ';' after import");fe_node_add(root,i);} + while(!is(p,FE_TOK_EOF)){FeNode *d=decl(p);if(d)fe_node_add(root,d);} + return root; +} diff --git a/fec/src/parser.h b/fec/src/parser.h new file mode 100644 index 0000000..a0d5ef7 --- /dev/null +++ b/fec/src/parser.h @@ -0,0 +1,17 @@ +#ifndef FE_PARSER_H +#define FE_PARSER_H + +#include "ast.h" + +typedef struct FeParser { + FeLexer lexer; + FeToken current; + FeToken previous; + FeAst *ast; + FeDiags *diags; +} FeParser; + +void fe_parser_init(FeParser *p, FeAst *ast, const char *src, unsigned long length, const char *file, FeDiags *d); +FeNode *fe_parse_unit(FeParser *p); + +#endif diff --git a/fec/std/core.fe b/fec/std/core.fe new file mode 100644 index 0000000..38a3c58 --- /dev/null +++ b/fec/std/core.fe @@ -0,0 +1,5 @@ +unit core; + +pub error Error { Invalid = 1, Io = 2, } +pub fn panic(msg: str, file: str, line: u32) { } +pub fn assert(ok: bool) { } diff --git a/fec/std/fmt.fe b/fec/std/fmt.fe new file mode 100644 index 0000000..460cc0b --- /dev/null +++ b/fec/std/fmt.fe @@ -0,0 +1,4 @@ +unit fmt; +pub fn write_str(w: &mut io.Writer, s: str) -> !void; +pub fn write_int_i32(w: &mut io.Writer, v: i32) -> !void; +pub fn write_bool(w: &mut io.Writer, v: bool) -> !void; diff --git a/fec/std/io.fe b/fec/std/io.fe new file mode 100644 index 0000000..2f4f983 --- /dev/null +++ b/fec/std/io.fe @@ -0,0 +1,9 @@ +unit io; +pub struct Writer { + ctx: *void, + write_fn: fn(*void, []u8) -> !usize, +} +pub struct File { + handle: u16, + pub fn close(self: &mut Self) { } +} diff --git a/fec/std/list.fe b/fec/std/list.fe new file mode 100644 index 0000000..d414df7 --- /dev/null +++ b/fec/std/list.fe @@ -0,0 +1,6 @@ +unit list; +pub struct List(T) { + items: ^[]T, + len: usize, + pub fn at(self: &Self, i: usize) -> &T; +} diff --git a/fec/std/map.fe b/fec/std/map.fe new file mode 100644 index 0000000..5e238c6 --- /dev/null +++ b/fec/std/map.fe @@ -0,0 +1,4 @@ +unit map; +pub struct Map(K, V) { + len: usize, +} diff --git a/fec/std/mem.fe b/fec/std/mem.fe new file mode 100644 index 0000000..cec54f5 --- /dev/null +++ b/fec/std/mem.fe @@ -0,0 +1,11 @@ +unit mem; + +pub fn create(T: type) -> !^T; +pub fn destroy(p: *void); +pub fn copy(dst: []u8, src: []u8); +pub struct Arena { + ptr: *void, + pub fn init() -> Arena { return Arena{ ptr: null }; } + pub fn reset(self: &mut Self) { } + pub fn drop(self: &mut Self) { } +} diff --git a/fec/std/str.fe b/fec/std/str.fe new file mode 100644 index 0000000..4d5d7f0 --- /dev/null +++ b/fec/std/str.fe @@ -0,0 +1,3 @@ +unit str; +pub fn eq(a: str, b: str) -> bool; +pub fn trim(s: str) -> str; diff --git a/fec/std/sys.fe b/fec/std/sys.fe new file mode 100644 index 0000000..d9003a8 --- /dev/null +++ b/fec/std/sys.fe @@ -0,0 +1,2 @@ +unit sys; +pub fn exit(code: u16); diff --git a/fec/test-dos.bat b/fec/test-dos.bat new file mode 100644 index 0000000..36935d8 --- /dev/null +++ b/fec/test-dos.bat @@ -0,0 +1,51 @@ +@echo off +rem FreeDOS smoke tests. All work happens on the writable C: drive. +C: +cd \FEC +if exist TEST.OK del TEST.OK +if exist TEST.FAIL del TEST.FAIL +call C:\FEC\BUILD.BAT +if not exist BUILD.OK goto test_fail + +fec.exe --dump-ast TESTS\PASS\BASIC.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast TESTS\PASS\LITERALS.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast TESTS\PASS\KEYWOR.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast TESTS\PASS\V012-F.FE > nul +if errorlevel 1 goto test_fail + +fec.exe --dump-ast STD\CORE.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast STD\FMT.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast STD\IO.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast STD\LIST.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast STD\MAP.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast STD\MEM.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast STD\STR.FE > nul +if errorlevel 1 goto test_fail +fec.exe --dump-ast STD\SYS.FE > nul +if errorlevel 1 goto test_fail + +fec.exe --dump-ast TESTS\FAIL\MISSIN.FE > nul +if not errorlevel 1 goto test_fail +fec.exe --dump-ast TESTS\FAIL\UNCLOS.FE > nul +if not errorlevel 1 goto test_fail +fec.exe --dump-ast TESTS\FAIL\LOGICA.FE > nul +if not errorlevel 1 goto test_fail + +echo OK>TEST.OK +cd C:\FEC +goto test_done + +:test_fail +echo FAIL>TEST.FAIL +verify other 2>nul + +:test_done diff --git a/fec/tests/fail/logical-symbols.fe b/fec/tests/fail/logical-symbols.fe new file mode 100644 index 0000000..a38bcc6 --- /dev/null +++ b/fec/tests/fail/logical-symbols.fe @@ -0,0 +1,3 @@ +// ERROR:logical operator +unit old_logic; +fn main() { let x = true && false; } diff --git a/fec/tests/fail/missing-semi.fe b/fec/tests/fail/missing-semi.fe new file mode 100644 index 0000000..c767019 --- /dev/null +++ b/fec/tests/fail/missing-semi.fe @@ -0,0 +1,3 @@ +// ERROR:expected ';' +unit broken; +fn main() { let x: i32 = 1 } diff --git a/fec/tests/fail/unclosed-comment.fe b/fec/tests/fail/unclosed-comment.fe new file mode 100644 index 0000000..fe95c5a --- /dev/null +++ b/fec/tests/fail/unclosed-comment.fe @@ -0,0 +1,3 @@ +// ERROR:unterminated block comment +unit broken; +/* no ending delimiter diff --git a/fec/tests/pass/basic.fe b/fec/tests/pass/basic.fe new file mode 100644 index 0000000..86d4062 --- /dev/null +++ b/fec/tests/pass/basic.fe @@ -0,0 +1,25 @@ +unit basic; +import core; + +/* outer comment /* nested comment */ still active */ +const LIMIT: u16 = 1_000; +pub struct Point { + pub x: i32, + y: i32, + pub fn new(x: i32, y: i32) -> Point { return Point{ x: x, y: y }; } + pub fn shift(self: &mut Self, dx: i32) { self.x += dx; } +} +pub enum Shape { + Empty, + Circle(i32), + Rect{ w: i32, h: i32 }, +} +pub error IoError { NotFound = 1, Denied = 2, } + +pub fn main() -> !void { + let p: Point = Point{ x: 1, y: 2 }; + if p.x > 0 and true { p.shift(1); } else { p.x = 0; } + while p.x < 10 { p.x += 1; if p.x == 5 { continue; } } + for i, x in p.x..10 { let _n: usize = i; let _q = x; } + return; +} diff --git a/fec/tests/pass/keywords-and-builtins.fe b/fec/tests/pass/keywords-and-builtins.fe new file mode 100644 index 0000000..21b231d --- /dev/null +++ b/fec/tests/pass/keywords-and-builtins.fe @@ -0,0 +1,9 @@ +unit keywords_and_builtins; + +pub fn demo() { + let a = true and not false; + let b = a or false; + @print("selected branch"); + let p = @as_far_fn(handler); + @call_far(p); +} diff --git a/fec/tests/pass/literals.fe b/fec/tests/pass/literals.fe new file mode 100644 index 0000000..775e9be --- /dev/null +++ b/fec/tests/pass/literals.fe @@ -0,0 +1,12 @@ +unit literals; +const A: u32 = 0xFF; +const B: u16 = 0b1010; +const C: u16 = 0o17; +const D: u32 = 1_000_000; +pub fn strings() { + let a = "hello\\nworld"; + let b = '\x41'; + let c = true; + let d = null; + let e = a orelse "fallback"; +} diff --git a/fec/tests/pass/v012-forms.fe b/fec/tests/pass/v012-forms.fe new file mode 100644 index 0000000..db3d4b5 --- /dev/null +++ b/fec/tests/pass/v012-forms.fe @@ -0,0 +1,20 @@ +unit v012_forms; + +shared atomic var ticks: u16 = 0; +packed struct Packet { + tag: u8, + value: u16, +} +interrupt_safe fn poll() { } +interrupt fn timer() { } +fn invoke(p: far fn()) { } + +pub fn demo() { + var count = undefined; + count = 1; + critical { ticks += 1; } + let x = true and not false or false; + let y = x orelse true; + let e = error.NotFound; + @call_far(@as_far_fn(timer)); +} diff --git a/fec/tests/run-tests.sh b/fec/tests/run-tests.sh new file mode 100644 index 0000000..6b4eef7 --- /dev/null +++ b/fec/tests/run-tests.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu +root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +ok=0 +for f in "$root"/std/*.fe; do + [ -f "$f" ] || continue + "$root"/fec --dump-ast "$f" >/dev/null || { echo "FAIL: $f"; exit 1; } + ok=$((ok+1)) +done +for f in "$root"/tests/pass/*.fe; do + [ -f "$f" ] || continue + "$root"/fec --dump-ast "$f" >/dev/null || { echo "FAIL: $f"; exit 1; } + ok=$((ok+1)) +done +for f in "$root"/tests/fail/*.fe; do + [ -f "$f" ] || continue + if "$root"/fec --dump-ast "$f" >/dev/null 2>/dev/null; then echo "FAIL (accepted): $f"; exit 1; fi + ok=$((ok+1)) +done +echo "M1 tests: $ok cases passed" diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat new file mode 100644 index 0000000..241d530 --- /dev/null +++ b/fec/vm-m1.bat @@ -0,0 +1,86 @@ +@echo off +rem D: is the read-only exchange volume. Stage everything before running DOS tools. +if not exist C:\FEC md C:\FEC +if not exist C:\FEC\SRC md C:\FEC\SRC +if not exist C:\FEC\STD md C:\FEC\STD +if not exist C:\FEC\TESTS md C:\FEC\TESTS +if not exist C:\FEC\TESTS\PASS md C:\FEC\TESTS\PASS +if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL +if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL + +copy D:\FEC\BUILD-~1.BAT C:\FEC\BUILD.BAT > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TEST-DOS.BAT C:\FEC\TEST-DOS.BAT > nul +if errorlevel 1 goto stage_fail + +copy D:\FEC\SRC\ARENA.C C:\FEC\SRC\ARENA.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\ARENA.H C:\FEC\SRC\ARENA.H > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\DIAG.C C:\FEC\SRC\DIAG.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\DIAG.H C:\FEC\SRC\DIAG.H > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\LEXER.C C:\FEC\SRC\LEXER.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\LEXER.H C:\FEC\SRC\LEXER.H > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\AST.C C:\FEC\SRC\AST.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\AST.H C:\FEC\SRC\AST.H > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\PARSER.C C:\FEC\SRC\PARSER.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\PARSER.H C:\FEC\SRC\PARSER.H > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\DRIVER.C C:\FEC\SRC\DRIVER.C > nul +if errorlevel 1 goto stage_fail + +copy D:\FEC\STD\CORE.FE C:\FEC\STD\CORE.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\STD\FMT.FE C:\FEC\STD\FMT.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\STD\IO.FE C:\FEC\STD\IO.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\STD\LIST.FE C:\FEC\STD\LIST.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\STD\MAP.FE C:\FEC\STD\MAP.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\STD\MEM.FE C:\FEC\STD\MEM.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\STD\STR.FE C:\FEC\STD\STR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\STD\SYS.FE C:\FEC\STD\SYS.FE > nul +if errorlevel 1 goto stage_fail + +copy D:\FEC\TESTS\PASS\BASIC.FE C:\FEC\TESTS\PASS\BASIC.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\PASS\LITERALS.FE C:\FEC\TESTS\PASS\LITERALS.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\PASS\KEYWOR~1.FE C:\FEC\TESTS\PASS\KEYWOR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\PASS\V012-F~1.FE C:\FEC\TESTS\PASS\V012-F.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\FAIL\MISSIN~1.FE C:\FEC\TESTS\FAIL\MISSIN.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\FAIL\UNCLOS~1.FE C:\FEC\TESTS\FAIL\UNCLOS.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\FAIL\LOGICA~1.FE C:\FEC\TESTS\FAIL\LOGICA.FE > nul +if errorlevel 1 goto stage_fail + +call C:\FEC\TEST-DOS.BAT +if exist C:\FEC\TEST.OK goto vm_success +echo FAIL>C:\FEC\VM.FAIL +verify other 2>nul +goto stage_done + +:vm_success +cd C:\FEC +goto stage_done + +:stage_fail +echo FAIL>C:\FEC\STAGE.FAIL +echo FAIL>C:\FEC\VM.FAIL +verify other 2>nul + +:stage_done From da78a615fbfcc0b32ce0c74e954bd84f7efe3ac5 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 07:40:50 +0900 Subject: [PATCH 005/184] feat: add M2 type checking and C emission --- fec/Makefile | 2 +- fec/build-dos.bat | 27 +- fec/src/ast.c | 2 +- fec/src/ast.h | 4 + fec/src/check.c | 515 ++++++++++++++++++++++++++++++++++ fec/src/check.h | 19 ++ fec/src/driver.c | 14 +- fec/src/emit_c.c | 280 ++++++++++++++++++ fec/src/emit_c.h | 16 ++ fec/src/parser.c | 4 +- fec/src/types.c | 50 ++++ fec/src/types.h | 31 ++ fec/test-dos.bat | 53 ++++ fec/tests/m2/bad-arity.fe | 9 + fec/tests/m2/bad-assign.fe | 7 + fec/tests/m2/bad-cast.fe | 6 + fec/tests/m2/bad-condition.fe | 6 + fec/tests/m2/bad-return.fe | 5 + fec/tests/m2/bad-types.fe | 9 + fec/tests/m2/bad-uninit.fe | 6 + fec/tests/m2/bad-unknown.fe | 5 + fec/tests/m2/bad-void.fe | 10 + fec/tests/m2/cast-while.fe | 11 + fec/tests/m2/hello.fe | 23 ++ fec/tests/m2/scopes.fe | 15 + fec/tests/run-tests.sh | 22 ++ fec/vm-m1.bat | 38 +++ 27 files changed, 1180 insertions(+), 9 deletions(-) create mode 100644 fec/src/check.c create mode 100644 fec/src/check.h create mode 100644 fec/src/emit_c.c create mode 100644 fec/src/emit_c.h create mode 100644 fec/src/types.c create mode 100644 fec/src/types.h create mode 100644 fec/tests/m2/bad-arity.fe create mode 100644 fec/tests/m2/bad-assign.fe create mode 100644 fec/tests/m2/bad-cast.fe create mode 100644 fec/tests/m2/bad-condition.fe create mode 100644 fec/tests/m2/bad-return.fe create mode 100644 fec/tests/m2/bad-types.fe create mode 100644 fec/tests/m2/bad-uninit.fe create mode 100644 fec/tests/m2/bad-unknown.fe create mode 100644 fec/tests/m2/bad-void.fe create mode 100644 fec/tests/m2/cast-while.fe create mode 100644 fec/tests/m2/hello.fe create mode 100644 fec/tests/m2/scopes.fe diff --git a/fec/Makefile b/fec/Makefile index 0891f8f..7f74ee8 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -std=c89 CPPFLAGS ?= -Isrc -SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/driver.c +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/check.c src/emit_c.c src/driver.c OBJ = $(SRC:.c=.o) .PHONY: all clean test dos-build diff --git a/fec/build-dos.bat b/fec/build-dos.bat index 510bbfa..42a1f5e 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -11,12 +11,37 @@ if exist diag.obj del diag.obj if exist lexer.obj del lexer.obj if exist ast.obj del ast.obj if exist parser.obj del parser.obj +if exist types.obj del types.obj +if exist check.obj del check.obj +if exist emitc.obj del emitc.obj +if exist emit_c.obj del emit_c.obj if exist driver.obj del driver.obj if "%WATCOM%"=="" set WATCOM=C:\DEVEL\WATCOMC if not exist %WATCOM%\BINW\WCL.EXE goto build_fail set PATH=%WATCOM%\BINW;%WATCOM%\BINP;%PATH% -wcl -q -za -wx -bt=dos -k32768 -fe=fec.exe src\arena.c src\diag.c src\lexer.c src\ast.c src\parser.c src\driver.c +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=arena.obj src\arena.c +if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=diag.obj src\diag.c +if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lexer.obj src\lexer.c +if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=ast.obj src\ast.c +if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=parser.obj src\parser.c +if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=types.obj src\types.c +if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check.c +if errorlevel 1 goto build_fail +rem Use an unambiguous short object name for the emit_c source. +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emit_c.c +if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=driver.obj src\driver.c +if errorlevel 1 goto build_fail +rem DOS command lines are limited to roughly 126 characters. All stale objects +rem were removed above, so the wildcard contains exactly this build's objects. +wcl -q -za -wx -bt=dos -ml -k32768 -fe=fec.exe *.obj if errorlevel 1 goto build_fail if not exist fec.exe goto build_fail echo OK>BUILD.OK diff --git a/fec/src/ast.c b/fec/src/ast.c index 81b0e14..798c4ed 100644 --- a/fec/src/ast.c +++ b/fec/src/ast.c @@ -8,7 +8,7 @@ FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned lo FeNode *n=(FeNode *)fe_arena_alloc(&a->arena,sizeof(FeNode)); if (!n) return 0; n->kind=k; n->loc=loc; n->text=text?fe_arena_strdup(&a->arena,text,len):0; - n->a=n->b=n->c=n->children=n->next=0; return n; + n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->sem_type=0; return n; } void fe_node_add(FeNode *parent, FeNode *child) { diff --git a/fec/src/ast.h b/fec/src/ast.h index 3b26b02..535d4ce 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -15,6 +15,7 @@ typedef enum FeNodeKind { } FeNodeKind; typedef struct FeNode FeNode; +typedef struct FeType FeType; struct FeNode { FeNodeKind kind; FeLoc loc; @@ -24,6 +25,9 @@ struct FeNode { FeNode *c; FeNode *children; FeNode *next; + /* Semantic information filled by checking; kept out of AST dumps. */ + char *cname; + FeType *sem_type; }; typedef struct FeAst { diff --git a/fec/src/check.c b/fec/src/check.c new file mode 100644 index 0000000..536c149 --- /dev/null +++ b/fec/src/check.c @@ -0,0 +1,515 @@ +#include "check.h" +#include +#include + +typedef struct FeSym FeSym; +typedef struct FeScope FeScope; + +struct FeSym { + const char *name; + char *cname; + FeType *type; + FeNode *fn; + int mutable; + int initialized; +}; + +struct FeScope { + FeScope *parent; + FeSym *items; + unsigned count; + unsigned capacity; +}; + +typedef struct FeCheckerState { + FeCheck *c; + FeScope *scope; + FeScope *globals; + FeType *ret; +} FeCheckerState; + +static FeType *unknown(FeCheck *c) +{ + return fe_type_intern(&c->types, ""); +} + +static void err(FeCheck *c, FeLoc loc, const char *msg) +{ + fe_diag_error(c->diags, loc, msg); +} + +static int known(FeType *t) +{ + return t && t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ERROR; +} + +static int compatible(FeType *want, FeType *got, FeNode *value) +{ + if (fe_type_equal(want, got)) return 1; + if (!known(want) || !known(got)) return 1; + return fe_type_is_integer(want) && fe_type_is_integer(got) && value && + value->kind == FE_N_LITERAL && value->text && + value->text[0] != '\'' && value->text[0] != '"'; +} + +static FeType *node_type(FeCheck *c, FeNode *n) +{ + FeType *t; + if (!n) return unknown(c); + t = fe_type_from_ast(&c->types, n); + n->sem_type = t; + return t; +} + +static char *unit_cname(FeCheck *c, const char *name) +{ + char *u; + char *p; + unsigned long n; + u = c->ast->root && c->ast->root->text ? c->ast->root->text : "unit"; + n = (unsigned long)strlen("fe_") + (unsigned long)strlen(u) + + (unsigned long)strlen(name ? name : "name") + 2UL; + p = (char *)fe_arena_alloc(&c->ast->arena, n); + if (!p) return 0; + strcpy(p, "fe_"); + strcat(p, u); + strcat(p, "_"); + strcat(p, name ? name : "name"); + return p; +} + +static char *local_cname(FeCheck *c, const char *name) +{ + char number[24]; + char *p; + unsigned long n; + sprintf(number, "%u", c->local_serial++); + n = (unsigned long)strlen("fe_l_") + (unsigned long)strlen(name) + + (unsigned long)strlen(number) + 2UL; + p = (char *)fe_arena_alloc(&c->ast->arena, n); + if (!p) return 0; + strcpy(p, "fe_l_"); + strcat(p, name ? name : "local"); + strcat(p, "_"); + strcat(p, number); + return p; +} + +static FeScope *scope_new(FeCheckerState *s, FeScope *parent) +{ + FeScope *scope; + scope = (FeScope *)fe_arena_alloc(&s->c->ast->arena, sizeof(FeScope)); + if (!scope) { + err(s->c, s->c->ast->root->loc, "out of memory creating scope"); + return parent; + } + scope->parent = parent; + scope->items = 0; + scope->count = 0; + scope->capacity = 0; + return scope; +} + +static FeSym *find_current(FeScope *scope, const char *name) +{ + unsigned i; + if (!scope) return 0; + for (i = scope->count; i > 0; --i) + if (strcmp(scope->items[i - 1].name, name) == 0) + return &scope->items[i - 1]; + return 0; +} + +static FeSym *find_symbol(FeScope *scope, const char *name) +{ + FeSym *sym; + while (scope) { + sym = find_current(scope, name); + if (sym) return sym; + scope = scope->parent; + } + return 0; +} + +static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, + const char *name, FeType *type, FeNode *fn, + int mutable, int initialized, char *cname, + FeNode *decl) +{ + FeSym *items; + unsigned capacity; + FeSym *sym; + if (!name) name = ""; + if (find_current(scope, name)) { + err(s->c, decl ? decl->loc : s->c->ast->root->loc, + "duplicate declaration in scope"); + return 0; + } + if (scope->count == scope->capacity) { + capacity = scope->capacity ? scope->capacity * 2U : 8U; + items = (FeSym *)fe_arena_alloc(&s->c->ast->arena, + capacity * sizeof(FeSym)); + if (!items) { + err(s->c, decl ? decl->loc : s->c->ast->root->loc, + "out of memory growing symbol scope"); + return 0; + } + if (scope->items) + memcpy(items, scope->items, scope->count * sizeof(FeSym)); + scope->items = items; + scope->capacity = capacity; + } + sym = &scope->items[scope->count++]; + sym->name = name; + sym->cname = cname; + sym->type = type; + sym->fn = fn; + sym->mutable = mutable; + sym->initialized = initialized; + if (decl) { + decl->cname = cname; + decl->sem_type = type; + } + return sym; +} + +void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, + unsigned pointer_bits) +{ + c->ast = ast; + c->diags = diags; + c->pointer_bits = pointer_bits; + c->local_serial = 0; + fe_types_init(&c->types, &ast->arena, pointer_bits); +} + +static FeType *check_expr(FeCheckerState *s, FeNode *n); + +static FeType *check_identifier(FeCheckerState *s, FeNode *n, int read) +{ + FeSym *sym; + sym = find_symbol(s->scope, n->text ? n->text : ""); + if (!sym) { + err(s->c, n->loc, "unknown name"); + return unknown(s->c); + } + n->cname = sym->cname; + n->sem_type = sym->type; + if (read && !sym->initialized && !sym->fn) + err(s->c, n->loc, "use of uninitialized variable"); + return sym->type; +} + +static FeType *check_expr(FeCheckerState *s, FeNode *n) +{ + FeCheck *c = s->c; + FeType *a; + FeType *b; + FeSym *sym; + FeNode *x; + FeNode *param; + FeNode *arg; + const char *op; + if (!n) return unknown(c); + if (n->kind == FE_N_IDENT) + return check_identifier(s, n, 1); + if (n->kind == FE_N_LITERAL) { + if (!n->text) return unknown(c); + if (strcmp(n->text, "true") == 0 || strcmp(n->text, "false") == 0) + a = fe_type_intern(&c->types, "bool"); + else if (n->text[0] == '\'') + a = fe_type_intern(&c->types, "u8"); + else if (n->text[0] == '"') + a = unknown(c); + else + a = fe_type_intern(&c->types, "i32"); + n->sem_type = a; + return a; + } + if (n->kind == FE_N_UNARY) { + a = check_expr(s, n->a); + op = n->text ? n->text : ""; + if (strcmp(op, "not") == 0) { + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "'not' requires bool"); + a = fe_type_intern(&c->types, "bool"); + } else if (strcmp(op, "-") == 0) { + if (known(a) && !fe_type_is_integer(a)) + err(c, n->loc, "unary '-' requires integer"); + } + n->sem_type = a; + return a; + } + if (n->kind == FE_N_TYPE && n->text && strcmp(n->text, "as") == 0) { + a = check_expr(s, n->a); + b = node_type(c, n->b); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "cast target cannot be void"); + else if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b))) + err(c, n->loc, "'as' requires integer types"); + n->sem_type = b; + return b; + } + if (n->kind == FE_N_BINARY) { + a = check_expr(s, n->a); + b = check_expr(s, n->b); + op = n->text ? n->text : ""; + if (strcmp(op, "and") == 0 || strcmp(op, "or") == 0) { + if ((known(a) && a->kind != FE_TYPE_BOOL) || + (known(b) && b->kind != FE_TYPE_BOOL)) + err(c, n->loc, "logical operator requires bool operands"); + a = fe_type_intern(&c->types, "bool"); + } else if (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 || + strcmp(op, "<") == 0 || strcmp(op, "<=") == 0 || + strcmp(op, ">") == 0 || strcmp(op, ">=") == 0) { + if (known(a) && known(b) && !fe_type_equal(a, b) && + !compatible(a, b, n->b) && !compatible(b, a, n->a)) + err(c, n->loc, "comparison operands have different types"); + a = fe_type_intern(&c->types, "bool"); + } else { + if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b)) || + (known(a) && known(b) && !fe_type_equal(a, b) && + !compatible(a, b, n->b) && !compatible(b, a, n->a))) + err(c, n->loc, + "arithmetic operands must have the same integer type"); + } + n->sem_type = a; + return a; + } + if (n->kind == FE_N_CALL) { + if (n->a && n->a->kind == FE_N_IDENT) { + sym = find_symbol(s->scope, n->a->text ? n->a->text : ""); + if (!sym) { + err(c, n->loc, "unknown function"); + return unknown(c); + } + n->a->cname = sym->cname; + if (!sym->fn) { + err(c, n->loc, "name is not a function"); + return unknown(c); + } + param = sym->fn->a ? sym->fn->a->children : 0; + arg = n->children; + while (param && arg) { + a = check_expr(s, arg); + b = node_type(c, param->a); + if (!compatible(b, a, arg) && a->kind != FE_TYPE_UNKNOWN) + err(c, arg->loc, "argument type mismatch"); + param = param->next; + arg = arg->next; + } + if (param || arg) err(c, n->loc, "wrong number of arguments"); + a = sym->fn->b ? node_type(c, sym->fn->b) : + fe_type_intern(&c->types, "void"); + n->sem_type = a; + return a; + } + for (x = n->children; x; x = x->next) check_expr(s, x); + return unknown(c); + } + if (n->kind == FE_N_MEMBER) { + check_expr(s, n->a); + return unknown(c); + } + return unknown(c); +} + +static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) +{ + FeSym *sym; + if (n && n->kind == FE_N_IDENT) { + sym = find_symbol(s->scope, n->text ? n->text : ""); + if (!sym) { + err(s->c, n->loc, "unknown name"); + return unknown(s->c); + } + if (sym->fn) { + err(s->c, n->loc, "function is not assignable"); + return unknown(s->c); + } + if (!sym->mutable) + err(s->c, n->loc, "cannot assign to immutable let"); + n->cname = sym->cname; + n->sem_type = sym->type; + if (read && !sym->initialized) + err(s->c, n->loc, "use of uninitialized variable"); + return sym->type; + } + if (n) err(s->c, n->loc, "assignment requires a variable"); + return unknown(s->c); +} + +static int compound_operator(const char *op) +{ + return op && strcmp(op, "=") != 0; +} + +static void check_stmt(FeCheckerState *s, FeNode *n) +{ + FeCheck *c = s->c; + FeScope *old; + FeType *a; + FeType *b; + FeSym *sym; + FeNode *x; + int initialized; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + old = s->scope; + s->scope = scope_new(s, old); + for (x = n->children; x; x = x->next) check_stmt(s, x); + s->scope = old; + break; + case FE_N_LET: + case FE_N_CONST: + a = n->a ? node_type(c, n->a) : unknown(c); + b = check_expr(s, n->b); + if (!n->a) a = b; + if (a->kind == FE_TYPE_VOID) + err(c, n->loc, "variable cannot have void type"); + if (n->a && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "initializer type mismatch"); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "void expression cannot initialize a variable"); + add_symbol(s, s->scope, n->text, a, 0, 0, 1, + local_cname(c, n->text ? n->text : "local"), n); + break; + case FE_N_VAR: + a = n->a ? node_type(c, n->a) : unknown(c); + if (!n->b && !n->a) + err(c, n->loc, "uninitialized var requires an explicit type"); + b = n->b ? check_expr(s, n->b) : unknown(c); + if (!n->a && n->b) a = b; + if (a->kind == FE_TYPE_VOID) + err(c, n->loc, "variable cannot have void type"); + if (n->b && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "initializer type mismatch"); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "void expression cannot initialize a variable"); + initialized = n->b != 0; + add_symbol(s, s->scope, n->text, a, 0, 1, initialized, + local_cname(c, n->text ? n->text : "local"), n); + break; + case FE_N_ASSIGN: + b = check_expr(s, n->b); + a = check_lvalue(s, n->a, compound_operator(n->text)); + if (!compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "assignment type mismatch"); + sym = n->a && n->a->kind == FE_N_IDENT ? + find_symbol(s->scope, n->a->text) : 0; + if (sym && sym->mutable) sym->initialized = 1; + break; + case FE_N_EXPR_STMT: + check_expr(s, n->a); + break; + case FE_N_IF: + a = check_expr(s, n->a); + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "if condition must be bool"); + check_stmt(s, n->b); + check_stmt(s, n->c); + break; + case FE_N_WHILE: + a = check_expr(s, n->a); + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "while condition must be bool"); + check_stmt(s, n->b); + break; + case FE_N_RETURN: + b = n->a ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); + if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) + err(c, n->loc, "void expression returned from value function"); + else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && + b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "return type mismatch"); + break; + case FE_N_UNSAFE: + check_stmt(s, n->a); + break; + default: + break; + } +} + +static void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) +{ + FeCheckerState s; + FeScope *old; + FeNode *x; + FeType *t; + s.c = c; + s.globals = globals; + s.scope = scope_new(&s, globals); + s.ret = fn->b ? node_type(c, fn->b) : fe_type_intern(&c->types, "void"); + fn->sem_type = s.ret; + for (x = fn->a ? fn->a->children : 0; x; x = x->next) { + t = node_type(c, x->a); + if (t->kind == FE_TYPE_VOID) + err(c, x->loc, "parameter cannot have void type"); + add_symbol(&s, s.scope, x->text, t, 0, 1, 1, + local_cname(c, x->text ? x->text : "arg"), x); + } + old = s.scope; + if (fn->c) check_stmt(&s, fn->c); + s.scope = old; +} + +int fe_check_program(FeCheck *c) +{ + FeCheckerState s; + FeNode *n; + FeSym *sym; + FeType *t; + FeType *iv; + s.c = c; + s.scope = scope_new(&s, 0); + s.globals = s.scope; + s.ret = fe_type_intern(&c->types, "void"); + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { + if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { + t = n->a ? node_type(c, n->a) : unknown(c); + add_symbol(&s, s.globals, n->text, t, 0, + n->kind == FE_N_GLOBAL, n->b != 0, + unit_cname(c, n->text ? n->text : "global"), n); + } + } + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { + if (n->kind == FE_N_FN) { + t = fe_type_intern(&c->types, ""); + add_symbol(&s, s.globals, n->text, t, n, 0, 1, + unit_cname(c, n->text ? n->text : "fn"), n); + } + } + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { + if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { + sym = find_current(s.globals, n->text ? n->text : ""); + if (n->b) { + iv = check_expr(&s, n->b); + if (sym && sym->type->kind == FE_TYPE_UNKNOWN) { + sym->type = iv; + n->sem_type = iv; + } else if (sym && !compatible(sym->type, iv, n->b) && + iv->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "global initializer type mismatch"); + if (iv->kind == FE_TYPE_VOID) + err(c, n->loc, "void expression cannot initialize a global"); + } + } + } + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) + if (n->kind == FE_N_FN) check_fn(c, n, s.globals); + return c->diags->errors == 0; +} + +FeType *fe_check_expr_type(FeCheck *c, FeNode *n) +{ + FeCheckerState s; + s.c = c; + s.scope = scope_new(&s, 0); + s.globals = s.scope; + s.ret = fe_type_intern(&c->types, "void"); + return check_expr(&s, n); +} diff --git a/fec/src/check.h b/fec/src/check.h new file mode 100644 index 0000000..b30cb3c --- /dev/null +++ b/fec/src/check.h @@ -0,0 +1,19 @@ +#ifndef FE_CHECK_H +#define FE_CHECK_H + +#include "types.h" +#include "diag.h" + +typedef struct FeCheck { + FeAst *ast; + FeTypeCtx types; + FeDiags *diags; + unsigned pointer_bits; + unsigned local_serial; +} FeCheck; + +void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, unsigned pointer_bits); +int fe_check_program(FeCheck *c); +FeType *fe_check_expr_type(FeCheck *c, FeNode *n); + +#endif diff --git a/fec/src/driver.c b/fec/src/driver.c index 5c52c99..ad6d117 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -1,4 +1,6 @@ #include "parser.h" +#include "check.h" +#include "emit_c.h" #include #include #include @@ -12,14 +14,18 @@ static char *read_file(const char *name, unsigned long *size) if(n && fread(p,1,(size_t)n,f)!=(size_t)n){free(p);fclose(f);return 0;} fclose(f);p[n]='\0';*size=(unsigned long)n;return p; } static void usage(void) -{ puts("usage: fec [--dump-ast] file.fe [--target=bits16|bits32] [--model=small|large]"); } +{ puts("usage: fec [--dump-ast|--emit-c] file.fe [--target=bits16|bits32] [-o output.c]"); } int main(int argc, char **argv) { - int i,dump=0; const char *file=0; unsigned long n; char *src; FeDiags d; FeAst ast; FeParser p; + int i,dump=0,emit=0; const char *file=0,*outname=0; unsigned long n; char *src; FeDiags d; FeAst ast; FeParser p; FeCheck check; FeEmitter emitter; FILE *out; unsigned pointer_bits=32; + (void)emit; if(argc<2){usage();return 2;} - for(i=1;i=argc){fprintf(stderr,"fec: -o needs a path\n");return 2;}outname=argv[++i];} else if(strncmp(argv[i],"-o",2)==0 && argv[i][2]) outname=argv[i]+2; else if(strncmp(argv[i],"--target=bits16",15)==0) pointer_bits=16; else if(strncmp(argv[i],"--target=bits32",15)==0) pointer_bits=32; else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--no-checks")==0 || strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} else {fprintf(stderr,"fec: unknown option %s\n",argv[i]);return 2;} } if(!file){fprintf(stderr,"fec: no input file\n");return 2;} src=read_file(file,&n);if(!src)return 2;d.errors=0;d.warnings=0;fe_ast_init(&ast);fe_parser_init(&p,&ast,src,n,file,&d);ast.root=fe_parse_unit(&p); - if(dump) fe_ast_dump(ast.root,0,stdout); + if(dump) { fe_ast_dump(ast.root,0,stdout); fe_ast_destroy(&ast); free(src); return d.errors?1:0; } + fe_check_init(&check,&ast,&d,pointer_bits); if(!fe_check_program(&check)){fe_ast_destroy(&ast);free(src);return 1;} + out=outname?fopen(outname,"w"):stdout; if(!out){fprintf(stderr,"fec: cannot create %s\n",outname);fe_ast_destroy(&ast);free(src);return 2;} + fe_emit_c_init(&emitter,out,&check,pointer_bits);fe_emit_c_program(&emitter);if(outname)fclose(out); fe_ast_destroy(&ast); free(src); return d.errors?1:0; } diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c new file mode 100644 index 0000000..e696aea --- /dev/null +++ b/fec/src/emit_c.c @@ -0,0 +1,280 @@ +#include "emit_c.h" +#include +#include + +static void pad(FeEmitter *e) +{ + int i; + for (i = 0; i < e->indent; ++i) fputs(" ", e->out); +} + +static const char *ctype(FeEmitter *e, FeNode *n) +{ + FeType *t; + if (n && n->sem_type) t = n->sem_type; + else if (n) t = fe_type_from_ast(&e->check->types, n); + else t = fe_type_intern(&e->check->types, "i32"); + return fe_type_c_name(t, e->pointer_bits); +} + +static const char *cname(FeNode *n, const char *fallback) +{ + return n && n->cname ? n->cname : fallback; +} + +static void emit_expr(FeEmitter *e, FeNode *n); +static void emit_stmt(FeEmitter *e, FeNode *n); + +static void emit_expr(FeEmitter *e, FeNode *n) +{ + FeNode *x; + const char *op; + if (!n) { + fputs("0", e->out); + return; + } + switch (n->kind) { + case FE_N_IDENT: + fputs(cname(n, "fe_missing"), e->out); + break; + case FE_N_LITERAL: + if (n->text && strcmp(n->text, "true") == 0) fputs("1", e->out); + else if (n->text && strcmp(n->text, "false") == 0) fputs("0", e->out); + else fputs(n->text ? n->text : "0", e->out); + break; + case FE_N_UNARY: + op = n->text ? n->text : ""; + if (strcmp(op, "not") == 0) fputs("(!", e->out); + else { + fputc('(', e->out); + fputs(op, e->out); + } + emit_expr(e, n->a); + fputc(')', e->out); + break; + case FE_N_BINARY: + op = n->text ? n->text : "+"; + fputc('(', e->out); + emit_expr(e, n->a); + if (strcmp(op, "and") == 0) fputs(" && ", e->out); + else if (strcmp(op, "or") == 0) fputs(" || ", e->out); + else fputs(op, e->out); + emit_expr(e, n->b); + fputc(')', e->out); + break; + case FE_N_TYPE: + if (n->text && strcmp(n->text, "as") == 0) { + fputs("((", e->out); + fputs(ctype(e, n->b), e->out); + fputc(')', e->out); + emit_expr(e, n->a); + fputc(')', e->out); + } else emit_expr(e, n->a); + break; + case FE_N_CALL: + if (n->a) emit_expr(e, n->a); + else fputs(n->text ? n->text : "fe_builtin", e->out); + fputc('(', e->out); + for (x = n->children; x; x = x->next) { + if (x != n->children) fputs(", ", e->out); + emit_expr(e, x); + } + fputc(')', e->out); + break; + case FE_N_MEMBER: + emit_expr(e, n->a); + fputc('.', e->out); + if (n->b) fputs(n->b->text ? n->b->text : "member", e->out); + break; + default: + fputs("0", e->out); + break; + } +} + +static void emit_decl(FeEmitter *e, FeNode *n) +{ + pad(e); + fputs(ctype(e, n), e->out); + fputc(' ', e->out); + fputs(cname(n, "fe_local"), e->out); + fputs(";\n", e->out); +} + +static void emit_block(FeEmitter *e, FeNode *n) +{ + FeNode *x; + if (!n) { + pad(e); + fputs("{\n", e->out); + ++e->indent; + --e->indent; + pad(e); + fputc('}', e->out); + return; + } + pad(e); + fputs("{\n", e->out); + ++e->indent; + /* C89 requires declarations before statements in each actual block. */ + for (x = n->children; x; x = x->next) + if (x->kind == FE_N_LET || x->kind == FE_N_VAR) emit_decl(e, x); + for (x = n->children; x; x = x->next) emit_stmt(e, x); + --e->indent; + pad(e); + fputc('}', e->out); +} + +static void emit_stmt(FeEmitter *e, FeNode *n) +{ + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + emit_block(e, n); + fputc('\n', e->out); + break; + case FE_N_LET: + case FE_N_VAR: + if (n->b) { + pad(e); + fputs(cname(n, "fe_local"), e->out); + fputs(" = ", e->out); + emit_expr(e, n->b); + fputs(";\n", e->out); + } + break; + case FE_N_ASSIGN: + pad(e); + emit_expr(e, n->a); + fputc(' ', e->out); + fputs(n->text ? n->text : "=", e->out); + fputs(" ", e->out); + emit_expr(e, n->b); + fputs(";\n", e->out); + break; + case FE_N_EXPR_STMT: + pad(e); + emit_expr(e, n->a); + fputs(";\n", e->out); + break; + case FE_N_RETURN: + pad(e); + fputs("return", e->out); + if (n->a) { + fputc(' ', e->out); + emit_expr(e, n->a); + } + fputs(";\n", e->out); + break; + case FE_N_IF: + pad(e); + fputs("if (", e->out); + emit_expr(e, n->a); + fputs(") ", e->out); + if (n->b && n->b->kind == FE_N_BLOCK) emit_block(e, n->b); + else emit_block(e, 0); + if (n->c) { + fputs(" else ", e->out); + if (n->c->kind == FE_N_IF) emit_stmt(e, n->c); + else emit_block(e, n->c); + } + fputc('\n', e->out); + break; + case FE_N_WHILE: + pad(e); + fputs("while (", e->out); + emit_expr(e, n->a); + fputs(") ", e->out); + if (n->b && n->b->kind == FE_N_BLOCK) emit_block(e, n->b); + else emit_block(e, 0); + fputc('\n', e->out); + break; + default: + break; + } +} + +static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) +{ + FeNode *p; + const char *ret; + ret = fn->sem_type ? fe_type_c_name(fn->sem_type, e->pointer_bits) : + (fn->b ? ctype(e, fn->b) : "void"); + fputs(ret, e->out); + fputc(' ', e->out); + fputs(cname(fn, "fe_fn"), e->out); + fputc('(', e->out); + p = fn->a ? fn->a->children : 0; + if (!p) fputs("void", e->out); + while (p) { + if (p != fn->a->children) fputs(", ", e->out); + fputs(ctype(e, p->a), e->out); + fputc(' ', e->out); + fputs(cname(p, "fe_arg"), e->out); + p = p->next; + } + fputc(')', e->out); + if (prototype) fputs(";\n", e->out); + else { + fputs(" ", e->out); + emit_block(e, fn->c); + fputc('\n', e->out); + } +} + +static void emit_main_wrapper(FeEmitter *e, FeNode *fn) +{ + fputs("int main(void) {\n ", e->out); + if (fn->sem_type && fn->sem_type->kind == FE_TYPE_VOID) { + fputs(cname(fn, "fe_main"), e->out); + fputs("();\n return 0;\n", e->out); + } else { + fputs("return ", e->out); + fputs(cname(fn, "fe_main"), e->out); + fputs("();\n", e->out); + } + fputs("}\n", e->out); +} + +void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, + unsigned pointer_bits) +{ + e->out = out; + e->check = check; + e->pointer_bits = pointer_bits; + e->indent = 0; +} + +void fe_emit_c_program(FeEmitter *e) +{ + FeNode *n; + FeNode *main_fn = 0; + fputs("/* generated by fec M2 */\n#include \n\n", e->out); + for (n = e->check->ast->root ? e->check->ast->root->children : 0; + n; n = n->next) { + if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { + fputs(ctype(e, n), e->out); + fputc(' ', e->out); + fputs(cname(n, "fe_global"), e->out); + if (n->b) { + fputs(" = ", e->out); + emit_expr(e, n->b); + } + fputs(";\n", e->out); + } + } + for (n = e->check->ast->root ? e->check->ast->root->children : 0; + n; n = n->next) + if (n->kind == FE_N_FN) { + emit_fn(e, n, 1); + if (n->text && strcmp(n->text, "main") == 0) main_fn = n; + } + fputc('\n', e->out); + for (n = e->check->ast->root ? e->check->ast->root->children : 0; + n; n = n->next) + if (n->kind == FE_N_FN) emit_fn(e, n, 0); + if (main_fn) { + fputc('\n', e->out); + emit_main_wrapper(e, main_fn); + } +} diff --git a/fec/src/emit_c.h b/fec/src/emit_c.h new file mode 100644 index 0000000..0d2aa29 --- /dev/null +++ b/fec/src/emit_c.h @@ -0,0 +1,16 @@ +#ifndef FE_EMIT_C_H +#define FE_EMIT_C_H + +#include "check.h" + +typedef struct FeEmitter { + FILE *out; + FeCheck *check; + unsigned pointer_bits; + int indent; +} FeEmitter; + +void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, unsigned pointer_bits); +void fe_emit_c_program(FeEmitter *e); + +#endif diff --git a/fec/src/parser.c b/fec/src/parser.c index 4efa04d..434befa 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -172,8 +172,8 @@ static FeNode *statement(FeParser *p) { FeToken t=p->current; FeNode *n,*e; if(is(p,FE_TOK_LBRACE)) return block(p); - if(eat(p,FE_TOK_LET)) { n=toknode(p,FE_N_LET,t);if(is_name(p))next(p);else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in let");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } - if(eat(p,FE_TOK_VAR)) { n=toknode(p,FE_N_VAR,t);if(is_name(p))next(p);else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);if(eat(p,FE_TOK_EQ))n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_LET)) { n=toknode(p,FE_N_LET,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in let");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_VAR)) { n=toknode(p,FE_N_VAR,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);if(eat(p,FE_TOK_EQ))n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_CONST)) { n=toknode(p,FE_N_CONST,t);if(is_name(p))next(p);else error(p,"expected constant name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in const");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_IF)) { n=toknode(p,FE_N_IF,t);if(eat(p,FE_TOK_LET)){n->text=fe_arena_strdup(&p->ast->arena,"if let",6);if(is_name(p))next(p);if(eat(p,FE_TOK_LPAREN)){if(is_name(p))next(p);want(p,FE_TOK_RPAREN,"expected ')' in if let pattern");}want(p,FE_TOK_EQ,"expected '=' in if let");}n->a=expr(p,0);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } if(eat(p,FE_TOK_COMPTIME)) { n=toknode(p,FE_N_IF,t);want(p,FE_TOK_IF,"expected 'if' after comptime");n->text=fe_arena_strdup(&p->ast->arena,"comptime if",11);n->a=expr(p,0);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } diff --git a/fec/src/types.c b/fec/src/types.c new file mode 100644 index 0000000..37a17d0 --- /dev/null +++ b/fec/src/types.c @@ -0,0 +1,50 @@ +#include "types.h" +#include + +void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits) +{ ctx->arena=arena; ctx->types=0; ctx->pointer_bits=pointer_bits; } + +FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) +{ + FeType *t; unsigned i; unsigned bits=0; int uns=0; FeTypeKind kind=FE_TYPE_UNKNOWN; + if (!name) name=""; + for (t=ctx->types;t;t=t->next) if(strcmp(t->name,name)==0) return t; + if(strcmp(name,"void")==0) kind=FE_TYPE_VOID; + else if(strcmp(name,"bool")==0) kind=FE_TYPE_BOOL; + else if(strcmp(name,"i8")==0||strcmp(name,"u8")==0){kind=FE_TYPE_INT;bits=8;uns=name[0]=='u';} + else if(strcmp(name,"i16")==0||strcmp(name,"u16")==0){kind=FE_TYPE_INT;bits=16;uns=name[0]=='u';} + else if(strcmp(name,"i32")==0||strcmp(name,"u32")==0){kind=FE_TYPE_INT;bits=32;uns=name[0]=='u';} + else if(strcmp(name,"usize")==0||strcmp(name,"isize")==0){kind=FE_TYPE_INT;bits=ctx->pointer_bits;uns=name[0]=='u';} + t=(FeType *)fe_arena_alloc(ctx->arena,sizeof(FeType)); if(!t)return 0; + for(i=0;iname)-1 && name[i];i++) t->name[i]=name[i]; + t->name[i]='\0'; + t->kind=kind;t->bits=bits;t->is_unsigned=uns;t->next=ctx->types;ctx->types=t;return t; +} + +FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) +{ + if(!node)return fe_type_intern(ctx,""); + if(node->kind!=FE_N_TYPE)return fe_type_intern(ctx,""); + if(node->text && (strcmp(node->text,"?")==0||strcmp(node->text,"!")==0||strcmp(node->text,"^")==0||strcmp(node->text,"&")==0||strcmp(node->text,"*")==0||strcmp(node->text,"far")==0))return fe_type_intern(ctx,""); + if(node->text && strcmp(node->text,"as")==0)return fe_type_from_ast(ctx,node->b); + if(node->text && strcmp(node->text,"fn")==0)return fe_type_intern(ctx,""); + return fe_type_intern(ctx,node->text); +} +int fe_type_equal(const FeType *a,const FeType *b){return a==b || (a&&b&&strcmp(a->name,b->name)==0);} +int fe_type_is_integer(const FeType *t){return t&&t->kind==FE_TYPE_INT;} +const char *fe_type_c_name(const FeType *t,unsigned pointer_bits) +{ + if(!t)return "int"; + if(t->kind==FE_TYPE_VOID)return "void"; + if(t->kind==FE_TYPE_BOOL)return "unsigned char"; + if(t->kind!=FE_TYPE_INT)return "int"; + if(strcmp(t->name,"usize")==0)return pointer_bits==16?"unsigned short":"unsigned long"; + if(strcmp(t->name,"isize")==0)return pointer_bits==16?"short":"long"; + if(strcmp(t->name,"i8")==0)return "signed char"; + if(strcmp(t->name,"u8")==0)return "unsigned char"; + if(strcmp(t->name,"i16")==0)return "short"; + if(strcmp(t->name,"u16")==0)return "unsigned short"; + if(strcmp(t->name,"i32")==0)return "long"; + if(strcmp(t->name,"u32")==0)return "unsigned long"; + return "int"; +} diff --git a/fec/src/types.h b/fec/src/types.h new file mode 100644 index 0000000..290777f --- /dev/null +++ b/fec/src/types.h @@ -0,0 +1,31 @@ +#ifndef FE_TYPES_H +#define FE_TYPES_H + +#include "ast.h" + +typedef enum FeTypeKind { + FE_TYPE_ERROR, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_INT, FE_TYPE_UNKNOWN +} FeTypeKind; + +struct FeType { + FeTypeKind kind; + char name[16]; + unsigned bits; + int is_unsigned; + FeType *next; +}; + +typedef struct FeTypeCtx { + FeArena *arena; + FeType *types; + unsigned pointer_bits; +} FeTypeCtx; + +void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits); +FeType *fe_type_intern(FeTypeCtx *ctx, const char *name); +FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node); +int fe_type_equal(const FeType *a, const FeType *b); +int fe_type_is_integer(const FeType *t); +const char *fe_type_c_name(const FeType *t, unsigned pointer_bits); + +#endif diff --git a/fec/test-dos.bat b/fec/test-dos.bat index 36935d8..5427a5e 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -6,6 +6,10 @@ if exist TEST.OK del TEST.OK if exist TEST.FAIL del TEST.FAIL call C:\FEC\BUILD.BAT if not exist BUILD.OK goto test_fail +if "%WATCOM%"=="" set WATCOM=C:\DEVEL\WATCOMC +if not exist %WATCOM%\BINW\WCL.EXE goto test_fail +if not exist %WATCOM%\BINW\WCL386.EXE goto test_fail +set PATH=%WATCOM%\BINW;%WATCOM%\BINP;%PATH% fec.exe --dump-ast TESTS\PASS\BASIC.FE > nul if errorlevel 1 goto test_fail @@ -40,6 +44,55 @@ if not errorlevel 1 goto test_fail fec.exe --dump-ast TESTS\FAIL\LOGICA.FE > nul if not errorlevel 1 goto test_fail +if exist TESTS\M2\HELLO.C del TESTS\M2\HELLO.C +if exist TESTS\M2\HELLO.EXE del TESTS\M2\HELLO.EXE +if exist TESTS\M2\SCOPES.C del TESTS\M2\SCOPES.C +if exist TESTS\M2\SCOPES.EXE del TESTS\M2\SCOPES.EXE +if exist TESTS\M2\CAST16.C del TESTS\M2\CAST16.C +if exist TESTS\M2\CAST16.EXE del TESTS\M2\CAST16.EXE + +rem M2 bits32 path: Open Watcom 32-bit compiler and DOS extender executable. +fec.exe --target=bits32 --emit-c TESTS\M2\HELLO.FE -o TESTS\M2\HELLO.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M2\HELLO.EXE TESTS\M2\HELLO.C +if errorlevel 1 goto test_fail +TESTS\M2\HELLO.EXE +if errorlevel 1 goto test_fail + +fec.exe --target=bits32 --emit-c TESTS\M2\SCOPES.FE -o TESTS\M2\SCOPES.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M2\SCOPES.EXE TESTS\M2\SCOPES.C +if errorlevel 1 goto test_fail +TESTS\M2\SCOPES.EXE +if errorlevel 1 goto test_fail + +rem M2 bits16 regression path remains on compiler A (wcl). +fec.exe --target=bits16 --emit-c TESTS\M2\CAST-W.FE -o TESTS\M2\CAST16.C > nul +if errorlevel 1 goto test_fail +wcl -q -za -bt=dos -fe=TESTS\M2\CAST16.EXE TESTS\M2\CAST16.C +if errorlevel 1 goto test_fail +TESTS\M2\CAST16.EXE +if errorlevel 1 goto test_fail + +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CO.FE -o TESTS\M2\BAD-CO.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CA.FE -o TESTS\M2\BAD-CA.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-AS.FE -o TESTS\M2\BAD-AS.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UN.FE -o TESTS\M2\BAD-UN.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-AR.FE -o TESTS\M2\BAD-AR.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-TY.FE -o TESTS\M2\BAD-TY.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-RE.FE -o TESTS\M2\BAD-RE.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UI.FE -o TESTS\M2\BAD-UI.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-VO.FE -o TESTS\M2\BAD-VO.C > nul +if not errorlevel 1 goto test_fail + echo OK>TEST.OK cd C:\FEC goto test_done diff --git a/fec/tests/m2/bad-arity.fe b/fec/tests/m2/bad-arity.fe new file mode 100644 index 0000000..2ba4b2a --- /dev/null +++ b/fec/tests/m2/bad-arity.fe @@ -0,0 +1,9 @@ +unit bad_arity; + +fn add(a: i32, b: i32) -> i32 { + return a + b; +} + +fn main() -> i32 { + return add(1); +} diff --git a/fec/tests/m2/bad-assign.fe b/fec/tests/m2/bad-assign.fe new file mode 100644 index 0000000..fac907a --- /dev/null +++ b/fec/tests/m2/bad-assign.fe @@ -0,0 +1,7 @@ +unit bad_assign; + +fn main() -> i32 { + let value: i32 = 1; + value = 2; + return value; +} diff --git a/fec/tests/m2/bad-cast.fe b/fec/tests/m2/bad-cast.fe new file mode 100644 index 0000000..0eafcfc --- /dev/null +++ b/fec/tests/m2/bad-cast.fe @@ -0,0 +1,6 @@ +unit bad_cast; + +fn main() -> i32 { + let x: i32 = true as i32; + return x; +} diff --git a/fec/tests/m2/bad-condition.fe b/fec/tests/m2/bad-condition.fe new file mode 100644 index 0000000..2e706fa --- /dev/null +++ b/fec/tests/m2/bad-condition.fe @@ -0,0 +1,6 @@ +unit bad_condition; + +fn main() -> i32 { + if 1 { return 0; } + return 1; +} diff --git a/fec/tests/m2/bad-return.fe b/fec/tests/m2/bad-return.fe new file mode 100644 index 0000000..914cd99 --- /dev/null +++ b/fec/tests/m2/bad-return.fe @@ -0,0 +1,5 @@ +unit bad_return; + +fn main() -> i32 { + return true; +} diff --git a/fec/tests/m2/bad-types.fe b/fec/tests/m2/bad-types.fe new file mode 100644 index 0000000..1f277fe --- /dev/null +++ b/fec/tests/m2/bad-types.fe @@ -0,0 +1,9 @@ +unit bad_types; + +fn add(a: i32, b: i32) -> i32 { + return a + b; +} + +fn main() -> i32 { + return add(true, 1); +} diff --git a/fec/tests/m2/bad-uninit.fe b/fec/tests/m2/bad-uninit.fe new file mode 100644 index 0000000..51f6092 --- /dev/null +++ b/fec/tests/m2/bad-uninit.fe @@ -0,0 +1,6 @@ +unit bad_uninit; + +fn main() -> i32 { + var value: i32; + return value; +} diff --git a/fec/tests/m2/bad-unknown.fe b/fec/tests/m2/bad-unknown.fe new file mode 100644 index 0000000..51d1b01 --- /dev/null +++ b/fec/tests/m2/bad-unknown.fe @@ -0,0 +1,5 @@ +unit bad_unknown; + +fn main() -> i32 { + return missing_name; +} diff --git a/fec/tests/m2/bad-void.fe b/fec/tests/m2/bad-void.fe new file mode 100644 index 0000000..bf3ce1f --- /dev/null +++ b/fec/tests/m2/bad-void.fe @@ -0,0 +1,10 @@ +unit bad_void; + +fn noop() { + return; +} + +fn main() -> i32 { + let value: i32 = noop(); + return value; +} diff --git a/fec/tests/m2/cast-while.fe b/fec/tests/m2/cast-while.fe new file mode 100644 index 0000000..c54b114 --- /dev/null +++ b/fec/tests/m2/cast-while.fe @@ -0,0 +1,11 @@ +unit cast_while; + +pub fn main() -> i32 { + var x: i16 = 0; + while x < 3 { + x += 1; + } + let y: i32 = x as i32; + if y == 3 { return 0; } + return 1; +} diff --git a/fec/tests/m2/hello.fe b/fec/tests/m2/hello.fe new file mode 100644 index 0000000..aec68c7 --- /dev/null +++ b/fec/tests/m2/hello.fe @@ -0,0 +1,23 @@ +unit hello; + +fn add(a: i32, b: i32) -> i32 { + return a + b; +} + +fn is_answer(value: i32) -> bool { + return value == 42; +} + +fn touch() { + return; +} + +pub fn main() -> i32 { + touch(); + let value: i32 = add(20, 22); + if is_answer(value) { + return 0; + } else { + return 1; + } +} diff --git a/fec/tests/m2/scopes.fe b/fec/tests/m2/scopes.fe new file mode 100644 index 0000000..b0d6168 --- /dev/null +++ b/fec/tests/m2/scopes.fe @@ -0,0 +1,15 @@ +unit scopes; + +fn register(switch: i32) -> i32 { + let auto: i32 = switch; + if true { + let auto: i32 = auto + 1; + if auto == 2 { return 0; } + } + if auto == 1 { return 0; } + return 1; +} + +pub fn main() -> i32 { + return register(1); +} diff --git a/fec/tests/run-tests.sh b/fec/tests/run-tests.sh index 6b4eef7..d7b4d75 100644 --- a/fec/tests/run-tests.sh +++ b/fec/tests/run-tests.sh @@ -18,3 +18,25 @@ for f in "$root"/tests/fail/*.fe; do ok=$((ok+1)) done echo "M1 tests: $ok cases passed" + +m2tmp=$(mktemp -d) +trap 'rm -rf "$m2tmp"' EXIT HUP INT TERM +"$root"/fec --emit-c "$root"/tests/m2/hello.fe -o "$m2tmp/hello.c" +${CC:-cc} -std=c89 -pedantic "$m2tmp/hello.c" -o "$m2tmp/hello" +"$m2tmp/hello" +"$root"/fec --target=bits16 --emit-c "$root"/tests/m2/hello.fe -o "$m2tmp/hello16.c" +${CC:-cc} -std=c89 -pedantic "$m2tmp/hello16.c" -o "$m2tmp/hello16" +"$m2tmp/hello16" +"$root"/fec --emit-c "$root"/tests/m2/cast-while.fe -o "$m2tmp/cast.c" +${CC:-cc} -std=c89 -pedantic "$m2tmp/cast.c" -o "$m2tmp/cast" +"$m2tmp/cast" +"$root"/fec --emit-c "$root"/tests/m2/scopes.fe -o "$m2tmp/scopes.c" +${CC:-cc} -std=c89 -pedantic "$m2tmp/scopes.c" -o "$m2tmp/scopes" +"$m2tmp/scopes" +for f in bad-condition bad-cast bad-assign bad-unknown bad-arity bad-types bad-return bad-uninit bad-void; do + if "$root"/fec --emit-c "$root"/tests/m2/$f.fe -o "$m2tmp/$f.c" >/dev/null 2>/dev/null; then + echo "FAIL (accepted M2 semantic error): $f.fe" + exit 1 + fi +done +echo "M2 tests: integer control-flow smoke passed" diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat index 241d530..af2b722 100644 --- a/fec/vm-m1.bat +++ b/fec/vm-m1.bat @@ -6,7 +6,9 @@ if not exist C:\FEC\STD md C:\FEC\STD if not exist C:\FEC\TESTS md C:\FEC\TESTS if not exist C:\FEC\TESTS\PASS md C:\FEC\TESTS\PASS if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL +if not exist C:\FEC\TESTS\M2 md C:\FEC\TESTS\M2 if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL +if exist C:\FEC\STAGE.FAIL del C:\FEC\STAGE.FAIL copy D:\FEC\BUILD-~1.BAT C:\FEC\BUILD.BAT > nul if errorlevel 1 goto stage_fail @@ -35,6 +37,18 @@ copy D:\FEC\SRC\PARSER.H C:\FEC\SRC\PARSER.H > nul if errorlevel 1 goto stage_fail copy D:\FEC\SRC\DRIVER.C C:\FEC\SRC\DRIVER.C > nul if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\TYPES.C C:\FEC\SRC\TYPES.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\TYPES.H C:\FEC\SRC\TYPES.H > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\CHECK.C C:\FEC\SRC\CHECK.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\CHECK.H C:\FEC\SRC\CHECK.H > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\EMIT_C.C C:\FEC\SRC\EMIT_C.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\SRC\EMIT_C.H C:\FEC\SRC\EMIT_C.H > nul +if errorlevel 1 goto stage_fail copy D:\FEC\STD\CORE.FE C:\FEC\STD\CORE.FE > nul if errorlevel 1 goto stage_fail @@ -67,6 +81,30 @@ copy D:\FEC\TESTS\FAIL\UNCLOS~1.FE C:\FEC\TESTS\FAIL\UNCLOS.FE > nul if errorlevel 1 goto stage_fail copy D:\FEC\TESTS\FAIL\LOGICA~1.FE C:\FEC\TESTS\FAIL\LOGICA.FE > nul if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\HELLO.FE C:\FEC\TESTS\M2\HELLO.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\SCOPES.FE C:\FEC\TESTS\M2\SCOPES.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-CO~1.FE C:\FEC\TESTS\M2\BAD-CO.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-CAST.FE C:\FEC\TESTS\M2\BAD-CA.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-ASSI~1.FE C:\FEC\TESTS\M2\BAD-AS.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-UN~1.FE C:\FEC\TESTS\M2\BAD-UN.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-UN~2.FE C:\FEC\TESTS\M2\BAD-UI.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-AR~1.FE C:\FEC\TESTS\M2\BAD-AR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-TY~1.FE C:\FEC\TESTS\M2\BAD-TY.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-RE~1.FE C:\FEC\TESTS\M2\BAD-RE.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\BAD-VOID.FE C:\FEC\TESTS\M2\BAD-VO.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M2\CAST-W~1.FE C:\FEC\TESTS\M2\CAST-W.FE > nul +if errorlevel 1 goto stage_fail call C:\FEC\TEST-DOS.BAT if exist C:\FEC\TEST.OK goto vm_success From 3aa7618d0c129c9e04ec52a54dcec82552a6542d Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 08:16:24 +0900 Subject: [PATCH 006/184] docs: clarify char and byte conversions --- SPEC.AUDIT.md | 16 ++++++++++++++++ SPEC.md | 7 ++++--- 2 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 SPEC.AUDIT.md diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md new file mode 100644 index 0000000..bb85cc0 --- /dev/null +++ b/SPEC.AUDIT.md @@ -0,0 +1,16 @@ +# Ferro specification audit log + +`SPEC.md`를 항상 최신 규범 문서로 유지하고, 최초 `AUDIT.md` 반영 이후 설계 판단으로 +바뀐 사항은 이 파일에 누적한다. + +## 2026-08-16 — v0.1.3 + +### `char`와 `u8` 사이의 변환 + +- 문제: §4.1은 `char`를 `u8`과 별개 타입으로 규정하고 암묵 변환을 금지하지만, + §6.4의 줄 수 계산 예제는 `[]u8`에서 얻은 값을 문자 리터럴과 직접 비교했다. +- 결정: 별개 타입과 암묵 변환 금지 원칙을 유지한다. 저장, 대입, 비교 모두 명시적인 + `as`가 필요하며 문자 리터럴도 문맥에 따라 자동으로 `u8`이 되지 않는다. +- 명세 반영: 예제의 비교를 `c.^ == '\n' as u8`로 수정하고 §4.1에 규칙을 명시했다. +- 구현 영향: 타입 검사기는 `char`를 독립 기본 타입으로 취급해야 하고, C 방출 시 같은 + 크기의 정수 표현을 사용할 수 있어도 Ferro 단계에서는 `char`/`u8` 혼용을 거부해야 한다. diff --git a/SPEC.md b/SPEC.md index b4d387e..4c68c0a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.2 +# Ferro 언어 명세 v0.1.3 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -62,7 +62,8 @@ and or not orelse - 정수: `i8 i16 i32 u8 u16 u32 usize isize` - `bool` (1바이트, 정수와 상호 변환 없음) -- `char` (u8과 크기 같지만 별개 타입) +- `char` (`u8`과 크기 같지만 별개 타입). `char`와 `u8` 사이의 저장·대입·비교에는 + 반드시 명시적인 `as` 변환이 필요하며, 리터럴에도 문맥 기반 암묵 변환을 적용하지 않는다. - `void` (반환 타입으로만) - `type` (comptime 파라미터에서만, §9) @@ -415,7 +416,7 @@ fn count_lines(path: str) -> !usize { let got = try f.read(buf[..]); if got == 0 { break; } for c in buf[0..got] { - if c.^ == '\n' { n += 1; } + if c.^ == '\n' as u8 { n += 1; } } } return n; From 95de333da4281899dfd9ba375191b65b8eb5214a Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 08:33:19 +0900 Subject: [PATCH 007/184] docs: disambiguate match scrutinees --- SPEC.AUDIT.md | 12 ++++++++++++ SPEC.md | 5 ++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index bb85cc0..69e3742 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -14,3 +14,15 @@ - 명세 반영: 예제의 비교를 `c.^ == '\n' as u8`로 수정하고 §4.1에 규칙을 명시했다. - 구현 영향: 타입 검사기는 `char`를 독립 기본 타입으로 취급해야 하고, C 방출 시 같은 크기의 정수 표현을 사용할 수 있어도 Ferro 단계에서는 `char`/`u8` 혼용을 거부해야 한다. + +## 2026-08-16 — v0.1.4 + +### `match` scrutinee와 구조체 초기화의 중괄호 모호성 + +- 문제: `match expr { arms }`와 `Type{ fields }`가 모두 식별자 뒤에 `{`를 사용하므로 + `match value { ... }`의 arm 블록을 구조체 초기화로 잘못 소비할 수 있었다. +- 결정: `match` scrutinee 바로 뒤의 `{`는 항상 arm 블록으로 해석한다. 구조체 초기화식 + 자체를 scrutinee로 쓸 때는 `match (Type{ ... }) { ... }`처럼 괄호가 필수다. +- 구현 영향: match 문맥의 식 파서는 최상위 `{` 앞에서 scrutinee 파싱을 멈춰야 하며, + 괄호 안에서는 일반 구조체 초기화 규칙을 그대로 적용한다. 오류 복구는 모든 반복에서 + 적어도 한 토큰을 소비해 같은 진단을 무한 반복하지 않아야 한다. diff --git a/SPEC.md b/SPEC.md index 4c68c0a..fa81df6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.3 +# Ferro 언어 명세 v0.1.4 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -450,6 +450,9 @@ pub fn main() -> !void { - 이 루프 형태들은 경계 검사를 생략한다(컴파일러가 안전을 보장). - `while`은 `bool` 조건만. - `match`는 **완전성 검사**. 모든 배리언트를 다루거나 `_` 필요. +- `match expr { ... }`에서 scrutinee 바로 뒤의 `{`는 항상 arm 블록을 시작한다. + 따라서 구조체 초기화식을 scrutinee로 직접 쓸 때는 `match (Point{ x: 1, y: 2 }) { ... }`처럼 + 괄호로 감싸 구조체 초기화의 `{`를 명시한다. - `break`/`continue`는 가장 안쪽 루프에만 적용(레이블 없음). - `defer block`은 스코프 종료 시 역순 실행. 소멸자와 함께 선언 역순으로 병합 실행. `return`/`break`/에러 전파 경로에서도 실행. From 57b47a574aab3ddf3f17c1bba55438a18701814e Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 08:42:21 +0900 Subject: [PATCH 008/184] docs: disambiguate control-flow headers --- SPEC.AUDIT.md | 12 ++++++++++++ SPEC.md | 9 +++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index 69e3742..038ee87 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -26,3 +26,15 @@ - 구현 영향: match 문맥의 식 파서는 최상위 `{` 앞에서 scrutinee 파싱을 멈춰야 하며, 괄호 안에서는 일반 구조체 초기화 규칙을 그대로 적용한다. 오류 복구는 모든 반복에서 적어도 한 토큰을 소비해 같은 진단을 무한 반복하지 않아야 한다. + +## 2026-08-16 — v0.1.5 + +### 제어 흐름 헤더와 구조체 초기화의 중괄호 모호성 일반화 + +- 문제: v0.1.4의 모호성은 `match`뿐 아니라 `if flag {}`, `while flag {}` 및 + `for x in values {}`처럼 식 직후 본문이 시작되는 모든 제어 흐름에 동일하게 발생한다. +- 결정: `if`, `while`, `for`, `match`, `comptime if` 헤더 바로 뒤의 최상위 `{`는 항상 + 제어 흐름 블록을 시작한다. 헤더 최상위에 구조체 초기화식을 쓰려면 괄호가 필수다. +- 구현 영향: 제어 흐름 헤더의 최상위 식 파싱에서 구조체 초기화를 금지하되 괄호 안에서는 + 일반 식 파싱 상태를 복원한다. 단순 식별자 조건과 배열·슬라이스 반복은 본문 `{` 앞에서 + 정상적으로 종료되어야 한다. diff --git a/SPEC.md b/SPEC.md index fa81df6..1608898 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.4 +# Ferro 언어 명세 v0.1.5 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -450,9 +450,10 @@ pub fn main() -> !void { - 이 루프 형태들은 경계 검사를 생략한다(컴파일러가 안전을 보장). - `while`은 `bool` 조건만. - `match`는 **완전성 검사**. 모든 배리언트를 다루거나 `_` 필요. -- `match expr { ... }`에서 scrutinee 바로 뒤의 `{`는 항상 arm 블록을 시작한다. - 따라서 구조체 초기화식을 scrutinee로 직접 쓸 때는 `match (Point{ x: 1, y: 2 }) { ... }`처럼 - 괄호로 감싸 구조체 초기화의 `{`를 명시한다. +- `if`/`while`/`for`/`match`/`comptime if` 헤더 바로 뒤의 `{`는 항상 해당 제어 흐름의 + 본문 또는 arm 블록을 시작한다. 따라서 구조체 초기화식을 헤더의 최상위 식으로 직접 + 쓸 때는 `match (Point{ x: 1, y: 2 }) { ... }`처럼 괄호로 감싸 구조체 초기화의 `{`를 + 명시한다. 괄호 안의 구조체 초기화는 일반 식 규칙을 따른다. - `break`/`continue`는 가장 안쪽 루프에만 적용(레이블 없음). - `defer block`은 스코프 종료 시 역순 실행. 소멸자와 함께 선언 역순으로 병합 실행. `return`/`break`/에러 전파 경로에서도 실행. From 8e6a4096371e4dc96b40b622e5e0a438b5fe2714 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 08:49:50 +0900 Subject: [PATCH 009/184] feat: implement M3 aggregate types and iteration --- fec/build-dos.bat | 14 +- fec/src/ast.c | 4 +- fec/src/ast.h | 5 +- fec/src/check.c | 366 ++++++++++++++++++++++++++++- fec/src/check.h | 4 +- fec/src/driver.c | 8 +- fec/src/emit_c.c | 450 +++++++++++++++++++++++++++++++++-- fec/src/emit_c.h | 5 +- fec/src/parser.c | 116 +++++++-- fec/src/parser.h | 1 + fec/src/types.c | 495 ++++++++++++++++++++++++++++++++++++--- fec/src/types.h | 56 ++++- fec/test-dos.bat | 96 ++++++++ fec/tests/m3/array.fe | 9 + fec/tests/m3/arrayctx.fe | 7 + fec/tests/m3/badarr.fe | 5 + fec/tests/m3/badchar.fe | 6 + fec/tests/m3/badcycle.fe | 6 + fec/tests/m3/badfield.fe | 8 + fec/tests/m3/badfld.fe | 6 + fec/tests/m3/badindex.fe | 7 + fec/tests/m3/badmat.fe | 6 + fec/tests/m3/badstr.fe | 6 + fec/tests/m3/bounds.fe | 6 + fec/tests/m3/char.fe | 9 + fec/tests/m3/enum.fe | 16 ++ fec/tests/m3/for.fe | 18 ++ fec/tests/m3/nested.fe | 10 + fec/tests/m3/str.fe | 7 + fec/tests/m3/struct.fe | 14 ++ fec/tests/run-tests.sh | 21 ++ fec/vm-m1.bat | 36 +++ 32 files changed, 1729 insertions(+), 94 deletions(-) create mode 100644 fec/tests/m3/array.fe create mode 100644 fec/tests/m3/arrayctx.fe create mode 100644 fec/tests/m3/badarr.fe create mode 100644 fec/tests/m3/badchar.fe create mode 100644 fec/tests/m3/badcycle.fe create mode 100644 fec/tests/m3/badfield.fe create mode 100644 fec/tests/m3/badfld.fe create mode 100644 fec/tests/m3/badindex.fe create mode 100644 fec/tests/m3/badmat.fe create mode 100644 fec/tests/m3/badstr.fe create mode 100644 fec/tests/m3/bounds.fe create mode 100644 fec/tests/m3/char.fe create mode 100644 fec/tests/m3/enum.fe create mode 100644 fec/tests/m3/for.fe create mode 100644 fec/tests/m3/nested.fe create mode 100644 fec/tests/m3/str.fe create mode 100644 fec/tests/m3/struct.fe diff --git a/fec/build-dos.bat b/fec/build-dos.bat index 42a1f5e..38300c2 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -6,16 +6,10 @@ if exist BUILD.OK del BUILD.OK if exist BUILD.FAIL del BUILD.FAIL if exist fec.exe del fec.exe if exist __wcl__.lnk del __wcl__.lnk -if exist arena.obj del arena.obj -if exist diag.obj del diag.obj -if exist lexer.obj del lexer.obj -if exist ast.obj del ast.obj -if exist parser.obj del parser.obj -if exist types.obj del types.obj -if exist check.obj del check.obj -if exist emitc.obj del emitc.obj -if exist emit_c.obj del emit_c.obj -if exist driver.obj del driver.obj +rem WCL writes test objects into the current directory. Remove all prior build +rem artifacts before the wildcard link so 32-bit test objects cannot enter the +rem 16-bit compiler executable. +if exist *.obj del *.obj if "%WATCOM%"=="" set WATCOM=C:\DEVEL\WATCOMC if not exist %WATCOM%\BINW\WCL.EXE goto build_fail diff --git a/fec/src/ast.c b/fec/src/ast.c index 798c4ed..63fdff7 100644 --- a/fec/src/ast.c +++ b/fec/src/ast.c @@ -8,7 +8,7 @@ FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned lo FeNode *n=(FeNode *)fe_arena_alloc(&a->arena,sizeof(FeNode)); if (!n) return 0; n->kind=k; n->loc=loc; n->text=text?fe_arena_strdup(&a->arena,text,len):0; - n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->sem_type=0; return n; + n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->aux_text=0; n->aux_cname=0; n->sem_type=0; n->flags=0; return n; } void fe_node_add(FeNode *parent, FeNode *child) { @@ -33,7 +33,7 @@ void fe_ast_dump(const FeNode *n, int indent, FILE *out) } const char *fe_node_name(FeNodeKind k) { - static const char *names[] = {"unit","import","fn","struct","enum","error","const","global","field","param","variant","block","let","var","expr-stmt","assign","if","while","for","match","arm","return","break","continue","defer","unsafe","asm","type","expr","binary","unary","call","index","member","literal","ident","struct-init","error"}; + static const char *names[] = {"unit","import","fn","struct","enum","error","const","global","field","param","variant","block","let","var","expr-stmt","assign","if","while","for","match","arm","return","break","continue","defer","unsafe","asm","type","expr","binary","unary","call","index","member","literal","ident","struct-init","array-init","error"}; if ((unsigned)k >= sizeof(names)/sizeof(names[0])) return "node"; return names[k]; } diff --git a/fec/src/ast.h b/fec/src/ast.h index 535d4ce..33140e4 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -11,7 +11,7 @@ typedef enum FeNodeKind { FE_N_EXPR_STMT, FE_N_ASSIGN, FE_N_IF, FE_N_WHILE, FE_N_FOR, FE_N_MATCH, FE_N_ARM, FE_N_RETURN, FE_N_BREAK, FE_N_CONTINUE, FE_N_DEFER, FE_N_UNSAFE, FE_N_ASM, FE_N_TYPE, FE_N_EXPR, FE_N_BINARY, FE_N_UNARY, FE_N_CALL, FE_N_INDEX, FE_N_MEMBER, - FE_N_LITERAL, FE_N_IDENT, FE_N_STRUCT_INIT, FE_N_ERROR_NODE + FE_N_LITERAL, FE_N_IDENT, FE_N_STRUCT_INIT, FE_N_ARRAY_INIT, FE_N_ERROR_NODE } FeNodeKind; typedef struct FeNode FeNode; @@ -27,7 +27,10 @@ struct FeNode { FeNode *next; /* Semantic information filled by checking; kept out of AST dumps. */ char *cname; + char *aux_text; + char *aux_cname; FeType *sem_type; + unsigned flags; }; typedef struct FeAst { diff --git a/fec/src/check.c b/fec/src/check.c index 536c149..7714945 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -45,6 +45,25 @@ static int known(FeType *t) static int compatible(FeType *want, FeType *got, FeNode *value) { + FeNode *item; + unsigned long count; + if (want && got && value && value->kind==FE_N_ARRAY_INIT && + want->kind==FE_TYPE_ARRAY && got->kind==FE_TYPE_ARRAY) { + if (want->length != got->length) return 0; + count=0; + for (item=value->children; item; item=item->next) { + if (!compatible(want->elem,item->sem_type,item)) return 0; + if (item->kind==FE_N_LITERAL && item->text && + fe_type_is_integer(want->elem) && + fe_type_is_integer(item->sem_type) && + item->text[0]!='\'' && item->text[0]!='"') + item->sem_type=want->elem; + ++count; + } + if (count!=want->length) return 0; + value->sem_type=want; + return 1; + } if (fe_type_equal(want, got)) return 1; if (!known(want) || !known(got)) return 1; return fe_type_is_integer(want) && fe_type_is_integer(got) && value && @@ -52,6 +71,13 @@ static int compatible(FeType *want, FeType *got, FeNode *value) value->text[0] != '\'' && value->text[0] != '"'; } +static int explicit_castable(FeType *a, FeType *b) +{ + if (!a || !b) return 0; + return (fe_type_is_integer(a) || a->kind == FE_TYPE_CHAR) && + (fe_type_is_integer(b) || b->kind == FE_TYPE_CHAR); +} + static FeType *node_type(FeCheck *c, FeNode *n) { FeType *t; @@ -174,22 +200,114 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, } void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, - unsigned pointer_bits) + unsigned pointer_bits, int no_checks) { c->ast = ast; c->diags = diags; c->pointer_bits = pointer_bits; c->local_serial = 0; + c->no_checks = no_checks; fe_types_init(&c->types, &ast->arena, pointer_bits); + c->types.unit_name = ast->root && ast->root->text ? ast->root->text : "unit"; } static FeType *check_expr(FeCheckerState *s, FeNode *n); +static void check_match(FeCheckerState *s, FeNode *n); +static void check_stmt(FeCheckerState *s, FeNode *n); + +static int lvalue_writable(FeCheckerState *s, FeNode *n) +{ + FeSym *sym; + FeType *t; + if (!n) return 0; + if (n->kind == FE_N_IDENT) { + sym=find_symbol(s->scope,n->text ? n->text : ""); + return sym ? sym->mutable : 0; + } + if (n->kind == FE_N_MEMBER) { + t=n->a ? n->a->sem_type : 0; + if (t && t->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) return t->ref_mut; + return lvalue_writable(s,n->a); + } + if (n->kind == FE_N_INDEX) return lvalue_writable(s,n->a); + return 0; +} + +static int has_field(FeNode *list, const char *name) +{ + FeNode *f; + for (f=list; f; f=f->next) + if (f->text && name && strcmp(f->text,name)==0) return 1; + return 0; +} + +static FeType *check_struct_init(FeCheckerState *s, FeNode *n) +{ + FeType *t; + FeFieldType *field; + FeNode *f; + FeType *v; + FeType *et; + FeVariantType *variant; + unsigned i; + if (n->a && n->a->kind == FE_N_MEMBER) { + et=check_expr(s,n->a->a); + variant=et && et->kind==FE_TYPE_ENUM ? + fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; + if (!variant) { err(s->c,n->loc,"invalid enum variant"); return unknown(s->c); } + if (variant->field_count != 0) { + for (f=n->children; f; f=f->next) { + if (f->kind != FE_N_FIELD) continue; + field=0; + if (variant->fields) { + unsigned i; + for(i=0;ifield_count;i++) if(strcmp(variant->fields[i].name,f->text)==0) field=&variant->fields[i]; + } + if (!field) { err(s->c,f->loc,"invalid enum payload field"); continue; } + v=check_expr(s,f->a); + if (!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"enum payload type mismatch"); + } + } else if (n->children) err(s->c,n->loc,"empty enum variant cannot have payload"); + n->sem_type=et; return et; + } + t=fe_type_intern(&s->c->types,n->text ? n->text : ""); + if (!t || t->kind!=FE_TYPE_STRUCT) { err(s->c,n->loc,"unknown struct type"); return unknown(s->c); } + for(f=n->children;f;f=f->next) if(f->kind==FE_N_FIELD) { + if(has_field(f->next,f->text)) { err(s->c,f->loc,"duplicate struct field"); } + field=fe_type_field(t,f->text); + if(!field) { err(s->c,f->loc,"invalid struct field"); continue; } + v=check_expr(s,f->a); + if(!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"struct field type mismatch"); + } + for(i=0;ifield_count;i++) if(!has_field(n->children,t->fields[i].name)) err(s->c,n->loc,"missing struct field"); + n->sem_type=t; return t; +} + +static FeType *check_array_init(FeCheckerState *s, FeNode *n) +{ + FeNode *x; FeType *elem=0; FeType *v; unsigned long count=0; + for(x=n->children;x;x=x->next) { v=check_expr(s,x); if(!elem) elem=v; else if(!compatible(elem,v,x)&&v->kind!=FE_TYPE_UNKNOWN) err(s->c,x->loc,"array element type mismatch"); ++count; } + if(!elem) elem=unknown(s->c); + n->sem_type=fe_type_array(&s->c->types,count,elem); return n->sem_type; +} + +static FeType *check_index(FeCheckerState *s, FeNode *n) +{ + FeType *base=check_expr(s,n->a); FeType *idx; FeType *elem; + if(!fe_type_is_indexable(base)) { err(s->c,n->loc,"indexing requires an array or slice"); return unknown(s->c); } + if(n->b) { idx=check_expr(s,n->b); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"index must be an integer"); } + if(n->c || !n->b) { if(n->c) { idx=check_expr(s,n->c); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"slice bound must be an integer"); } elem=base->elem; n->sem_type=fe_type_slice(&s->c->types,elem); if(base->kind==FE_TYPE_STR) n->flags|=2U; return n->sem_type; } + n->sem_type=base->elem; return n->sem_type; +} static FeType *check_identifier(FeCheckerState *s, FeNode *n, int read) { FeSym *sym; sym = find_symbol(s->scope, n->text ? n->text : ""); if (!sym) { + FeType *named=fe_type_intern(&s->c->types,n->text ? n->text : ""); + if(named->kind==FE_TYPE_STRUCT || named->kind==FE_TYPE_ENUM) { n->sem_type=named; return named; } err(s->c, n->loc, "unknown name"); return unknown(s->c); } @@ -209,6 +327,9 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) FeNode *x; FeNode *param; FeNode *arg; + FeType *et; + FeFieldType *field; + FeVariantType *variant; const char *op; if (!n) return unknown(c); if (n->kind == FE_N_IDENT) @@ -218,14 +339,18 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) if (strcmp(n->text, "true") == 0 || strcmp(n->text, "false") == 0) a = fe_type_intern(&c->types, "bool"); else if (n->text[0] == '\'') - a = fe_type_intern(&c->types, "u8"); + a = fe_type_intern(&c->types, "char"); else if (n->text[0] == '"') - a = unknown(c); + a = fe_type_intern(&c->types, "str"); else a = fe_type_intern(&c->types, "i32"); n->sem_type = a; return a; } + if (n->kind == FE_N_STRUCT_INIT) return check_struct_init(s,n); + if (n->kind == FE_N_ARRAY_INIT) return check_array_init(s,n); + if (n->kind == FE_N_INDEX) return check_index(s,n); + if (n->kind == FE_N_MATCH) { check_match(s,n); n->sem_type=unknown(c); return n->sem_type; } if (n->kind == FE_N_UNARY) { a = check_expr(s, n->a); op = n->text ? n->text : ""; @@ -245,9 +370,8 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) b = node_type(c, n->b); if (b->kind == FE_TYPE_VOID) err(c, n->loc, "cast target cannot be void"); - else if ((known(a) && !fe_type_is_integer(a)) || - (known(b) && !fe_type_is_integer(b))) - err(c, n->loc, "'as' requires integer types"); + else if (known(a) && known(b) && !explicit_castable(a,b)) + err(c, n->loc, "'as' requires integer or char types"); n->sem_type = b; return b; } @@ -279,6 +403,24 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return a; } if (n->kind == FE_N_CALL) { + if (!n->a && n->text && (strcmp(n->text,"@size_of")==0 || strcmp(n->text,"@align_of")==0)) { + FeNode *type_arg=n->children; + FeType *target=type_arg && type_arg->kind==FE_N_IDENT ? fe_type_intern(&c->types,type_arg->text) : unknown(c); + if(!target || !known(target)) err(c,n->loc,"size/align requires a known type"); + n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; + } + if (n->a && n->a->kind == FE_N_MEMBER) { + et=check_expr(s,n->a->a); + variant=et && et->kind==FE_TYPE_ENUM ? + fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; + arg=n->children; + if (!variant) { err(c,n->loc,"invalid enum variant constructor"); return unknown(c); } + if (variant->field_count==1 && arg) { + FeType *av=check_expr(s,arg); + if(!compatible(variant->fields[0].type,av,arg)&&av->kind!=FE_TYPE_UNKNOWN) err(c,arg->loc,"enum payload type mismatch"); + } else if (variant->field_count != 0 || arg) err(c,n->loc,"wrong enum payload arity"); + n->sem_type=et; return et; + } if (n->a && n->a->kind == FE_N_IDENT) { sym = find_symbol(s->scope, n->a->text ? n->a->text : ""); if (!sym) { @@ -310,7 +452,25 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return unknown(c); } if (n->kind == FE_N_MEMBER) { - check_expr(s, n->a); + a=check_expr(s,n->a); + if (a->kind == FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=a->elem; + return a->elem; + } + if(a->kind==FE_TYPE_STRUCT) { + field=fe_type_field(a,n->b ? n->b->text : ""); + if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } + n->sem_type=field->type; return field->type; + } + if(a->kind==FE_TYPE_ENUM) { + if(!fe_type_variant(a,n->b ? n->b->text : "")) err(c,n->loc,"unknown enum variant"); + n->sem_type=a; return a; + } + if((a->kind==FE_TYPE_SLICE || a->kind==FE_TYPE_STR) && n->b && + strcmp(n->b->text,"n")==0) { + n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; + } return unknown(c); } return unknown(c); @@ -319,6 +479,8 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) { FeSym *sym; + FeType *base; + FeFieldType *field; if (n && n->kind == FE_N_IDENT) { sym = find_symbol(s->scope, n->text ? n->text : ""); if (!sym) { @@ -337,6 +499,32 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) err(s->c, n->loc, "use of uninitialized variable"); return sym->type; } + if (n && n->kind == FE_N_MEMBER) { + base=check_expr(s,n->a); + if (base && base->kind == FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + if (!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + n->sem_type=base->elem; + return base->elem; + } + if (!lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + field=base && base->kind==FE_TYPE_STRUCT ? fe_type_field(base,n->b ? n->b->text : "") : 0; + if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } + n->sem_type=field->type; return field->type; + } + if (n && n->kind == FE_N_INDEX) { + if (n->a && n->a->sem_type && n->a->sem_type->kind == FE_TYPE_STR) { + err(s->c, n->loc, "str is immutable"); + } + if (n->a && n->a->kind == FE_N_INDEX && (n->a->flags & 2U)) + err(s->c, n->loc, "str slice is immutable"); + if (!lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + base=check_index(s,n); + return base; + } if (n) err(s->c, n->loc, "assignment requires a variable"); return unknown(s->c); } @@ -346,6 +534,153 @@ static int compound_operator(const char *op) return op && strcmp(op, "=") != 0; } +static void check_match(FeCheckerState *s, FeNode *n) +{ + FeType *value; + FeNode *arm; + FeVariantType *variant; + int seen[256]; + int wildcard=0; + unsigned i; + for(i=0;i<256U;i++) seen[i]=0; + value=check_expr(s,n->a); + if(!value || value->kind!=FE_TYPE_ENUM) { err(s->c,n->loc,"match requires an enum value"); return; } + for(arm=n->children;arm;arm=arm->next) { + FeScope *old=s->scope; + if(arm->text && strcmp(arm->text,"_")==0) wildcard=1; + else { + variant=fe_type_variant(value,arm->text); + if(!variant) { err(s->c,arm->loc,"unknown match variant"); continue; } + if(variant->tag<256U) { + if(seen[variant->tag]) err(s->c,arm->loc,"duplicate match variant"); + seen[variant->tag]=1; + } + s->scope=scope_new(s,old); + if(variant->field_count==1 && arm->children) { + add_symbol(s,s->scope,arm->children->text,variant->fields[0].type,0,0,1, + local_cname(s->c,arm->children->text),arm->children); + } else if(variant->field_count>0) { + FeNode *b=arm->children; + for(i=0;ifield_count && b;i++,b=b->next) { + FeFieldType *f=&variant->fields[i]; + add_symbol(s,s->scope,b->text,f->type,0,0,1, + local_cname(s->c,b->text),b); + } + } + } + if(arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); + else if(arm->a) check_expr(s,arm->a); + s->scope=old; + } + if(!wildcard) for(i=0;ivariant_count && i<256U;i++) if(!seen[i]) err(s->c,n->loc,"non-exhaustive match"); +} + +static void check_for(FeCheckerState *s, FeNode *n) +{ + FeType *start; + FeType *finish; + FeType *elem; + FeType *ref_type; + FeSym *iter_sym; + char *index_cname; + char *item_cname; + int iter_mut; + FeScope *old=s->scope; + if(!n->c) { + start=check_expr(s,n->a); + if (!fe_type_is_indexable(start)) { + err(s->c,n->loc,"for iterable must be an array, slice, or str"); + return; + } + elem=start->elem; + iter_sym=0; + if (n->a && n->a->kind==FE_N_IDENT) + iter_sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); + else if (n->a && n->a->kind==FE_N_INDEX && n->a->a && + n->a->a->kind==FE_N_IDENT) + iter_sym=find_symbol(s->scope,n->a->a->text ? n->a->a->text : ""); + iter_mut=iter_sym && iter_sym->mutable; + if (start->kind==FE_TYPE_STR) iter_mut=0; + ref_type=fe_type_ref(&s->c->types,elem,iter_mut); + if (iter_mut) n->flags |= 4U; + s->scope=scope_new(s,old); + if (n->aux_text) { + index_cname=local_cname(s->c,n->text ? n->text : "index"); + item_cname=local_cname(s->c,n->aux_text); + add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, + index_cname,n); + add_symbol(s,s->scope,n->aux_text,ref_type,0,iter_mut,1, + item_cname,0); + n->cname=index_cname; + n->aux_cname=item_cname; + } else { + item_cname=local_cname(s->c,n->text ? n->text : "item"); + add_symbol(s,s->scope,n->text,ref_type,0,iter_mut,1, + item_cname,n); + n->cname=item_cname; + } + check_stmt(s,n->b); + s->scope=old; + return; + } + start=check_expr(s,n->a); + finish=check_expr(s,n->c); + if(known(start)&&!fe_type_is_integer(start)) err(s->c,n->loc,"range start must be integer"); + if(known(finish)&&!fe_type_is_integer(finish)) err(s->c,n->loc,"range end must be integer"); + s->scope=scope_new(s,old); + index_cname=local_cname(s->c,n->text ? n->text : "index"); + add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, + index_cname,n); + n->cname=index_cname; + check_stmt(s,n->b); + s->scope=old; +} + +static void check_type_cycle(FeCheck *c, FeType *t) +{ + unsigned i; + FeType *next; + if (!t || t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR || + t->kind == FE_TYPE_REF || + t->kind == FE_TYPE_INT || t->kind == FE_TYPE_BOOL || + t->kind == FE_TYPE_CHAR || t->kind == FE_TYPE_VOID || + t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) return; + if (t->cycle_state == 1) { + if (c->ast->root) err(c, c->ast->root->loc, "by-value recursive type"); + return; + } + if (t->cycle_state == 2) return; + t->cycle_state = 1; + if (t->kind == FE_TYPE_ARRAY) { + check_type_cycle(c,t->elem); + } else if (t->kind == FE_TYPE_STRUCT) { + for (i=0;ifield_count;i++) { + if (!t->fields[i].type && t->fields[i].ast_node) + t->fields[i].type=fe_type_from_ast(&c->types,t->fields[i].ast_node->a); + check_type_cycle(c,t->fields[i].type); + } + } else if (t->kind == FE_TYPE_ENUM) { + for (i=0;ivariant_count;i++) { + unsigned j; + for (j=0;jvariants[i].field_count;j++) { + if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) + t->variants[i].fields[j].type=fe_type_from_ast(&c->types, + t->variants[i].fields[j].ast_node->a); + next=t->variants[i].fields[j].type; + check_type_cycle(c,next); + } + } + } + t->cycle_state=2; +} + +static void check_type_cycles(FeCheck *c) +{ + FeType *t; + for (t=c->types.types;t;t=t->next) t->cycle_state=0; + for (t=c->types.types;t;t=t->next) check_type_cycle(c,t); +} + static void check_stmt(FeCheckerState *s, FeNode *n) { FeCheck *c = s->c; @@ -418,12 +753,19 @@ static void check_stmt(FeCheckerState *s, FeNode *n) err(c, n->loc, "while condition must be bool"); check_stmt(s, n->b); break; + case FE_N_FOR: + check_for(s,n); + break; + case FE_N_MATCH: + check_match(s,n); + break; case FE_N_RETURN: b = n->a ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) err(c, n->loc, "void expression returned from value function"); else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && - b->kind != FE_TYPE_UNKNOWN) + b->kind != FE_TYPE_UNKNOWN && + !compatible(s->ret,b,n->a)) err(c, n->loc, "return type mismatch"); break; case FE_N_UNSAFE: @@ -468,6 +810,13 @@ int fe_check_program(FeCheck *c) s.scope = scope_new(&s, 0); s.globals = s.scope; s.ret = fe_type_intern(&c->types, "void"); + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) + if (n->kind == FE_N_STRUCT) + fe_type_declare_struct(&c->types, n, (n->flags & 1U) != 0); + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) + if (n->kind == FE_N_ENUM) fe_type_declare_enum(&c->types, n); + check_type_cycles(c); + fe_type_layout_all(&c->types); for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { t = n->a ? node_type(c, n->a) : unknown(c); @@ -501,6 +850,7 @@ int fe_check_program(FeCheck *c) } for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) if (n->kind == FE_N_FN) check_fn(c, n, s.globals); + fe_type_layout_all(&c->types); return c->diags->errors == 0; } diff --git a/fec/src/check.h b/fec/src/check.h index b30cb3c..b26388a 100644 --- a/fec/src/check.h +++ b/fec/src/check.h @@ -10,9 +10,11 @@ typedef struct FeCheck { FeDiags *diags; unsigned pointer_bits; unsigned local_serial; + int no_checks; } FeCheck; -void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, unsigned pointer_bits); +void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, + unsigned pointer_bits, int no_checks); int fe_check_program(FeCheck *c); FeType *fe_check_expr_type(FeCheck *c, FeNode *n); diff --git a/fec/src/driver.c b/fec/src/driver.c index ad6d117..bfaa518 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -17,15 +17,15 @@ static void usage(void) { puts("usage: fec [--dump-ast|--emit-c] file.fe [--target=bits16|bits32] [-o output.c]"); } int main(int argc, char **argv) { - int i,dump=0,emit=0; const char *file=0,*outname=0; unsigned long n; char *src; FeDiags d; FeAst ast; FeParser p; FeCheck check; FeEmitter emitter; FILE *out; unsigned pointer_bits=32; + int i,dump=0,emit=0,no_checks=0; const char *file=0,*outname=0; unsigned long n; char *src; FeDiags d; FeAst ast; FeParser p; FeCheck check; FeEmitter emitter; FILE *out; unsigned pointer_bits=32; (void)emit; if(argc<2){usage();return 2;} - for(i=1;i=argc){fprintf(stderr,"fec: -o needs a path\n");return 2;}outname=argv[++i];} else if(strncmp(argv[i],"-o",2)==0 && argv[i][2]) outname=argv[i]+2; else if(strncmp(argv[i],"--target=bits16",15)==0) pointer_bits=16; else if(strncmp(argv[i],"--target=bits32",15)==0) pointer_bits=32; else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--no-checks")==0 || strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} else {fprintf(stderr,"fec: unknown option %s\n",argv[i]);return 2;} } + for(i=1;i=argc){fprintf(stderr,"fec: -o needs a path\n");return 2;}outname=argv[++i];} else if(strncmp(argv[i],"-o",2)==0 && argv[i][2]) outname=argv[i]+2; else if(strncmp(argv[i],"--target=bits16",15)==0) pointer_bits=16; else if(strncmp(argv[i],"--target=bits32",15)==0) pointer_bits=32; else if(strcmp(argv[i],"--no-checks")==0) no_checks=1; else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} else {fprintf(stderr,"fec: unknown option %s\n",argv[i]);return 2;} } if(!file){fprintf(stderr,"fec: no input file\n");return 2;} src=read_file(file,&n);if(!src)return 2;d.errors=0;d.warnings=0;fe_ast_init(&ast);fe_parser_init(&p,&ast,src,n,file,&d);ast.root=fe_parse_unit(&p); if(dump) { fe_ast_dump(ast.root,0,stdout); fe_ast_destroy(&ast); free(src); return d.errors?1:0; } - fe_check_init(&check,&ast,&d,pointer_bits); if(!fe_check_program(&check)){fe_ast_destroy(&ast);free(src);return 1;} + fe_check_init(&check,&ast,&d,pointer_bits,no_checks); if(!fe_check_program(&check)){fe_ast_destroy(&ast);free(src);return 1;} out=outname?fopen(outname,"w"):stdout; if(!out){fprintf(stderr,"fec: cannot create %s\n",outname);fe_ast_destroy(&ast);free(src);return 2;} - fe_emit_c_init(&emitter,out,&check,pointer_bits);fe_emit_c_program(&emitter);if(outname)fclose(out); + fe_emit_c_init(&emitter,out,&check,pointer_bits,no_checks);fe_emit_c_program(&emitter);if(outname)fclose(out); fe_ast_destroy(&ast); free(src); return d.errors?1:0; } diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index e696aea..04dd5f5 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -22,9 +22,305 @@ static const char *cname(FeNode *n, const char *fallback) return n && n->cname ? n->cname : fallback; } +static void emit_one_type(FeEmitter *e, FeType *t); + +static void emit_type_deps(FeEmitter *e, FeType *t) +{ + unsigned i,j; + if (!t) return; + if (t->kind == FE_TYPE_ARRAY) emit_one_type(e,t->elem); + if (t->kind == FE_TYPE_STRUCT) + for (i=0;ifield_count;i++) emit_one_type(e,t->fields[i].type); + if (t->kind == FE_TYPE_ENUM) + for (i=0;ivariant_count;i++) + for (j=0;jvariants[i].field_count;j++) + emit_one_type(e,t->variants[i].fields[j].type); +} + +static void emit_one_type(FeEmitter *e, FeType *t) +{ + unsigned i,j; + if (!t || t->emit_state || + (t->kind != FE_TYPE_STRUCT && t->kind != FE_TYPE_ENUM && + t->kind != FE_TYPE_ARRAY && t->kind != FE_TYPE_SLICE)) return; + t->emit_state=1; + emit_type_deps(e,t); + if(t->kind==FE_TYPE_STRUCT) { + fputs(t->cname,e->out); fputs(" {\n",e->out); + for(i=0;ifield_count;i++) { fputs(" ",e->out); fputs(fe_type_c_name(t->fields[i].type,e->pointer_bits),e->out); fputc(' ',e->out); fputs(t->fields[i].name,e->out); fputs(";\n",e->out); } + fputs("};\n",e->out); + } else if(t->kind==FE_TYPE_ARRAY) { + fputs(t->cname,e->out); fputs(" { ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" a[",e->out); fprintf(e->out,"%lu",t->length); fputs("]; };\n",e->out); + } else if(t->kind==FE_TYPE_SLICE && t->cname) { + fputs("typedef struct { ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); fputs(";\n",e->out); + fprintf(e->out,"static %s %s(%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n",t->cname,t->maker,fe_type_c_name(t->elem,e->pointer_bits),t->cname); + } else if(t->kind==FE_TYPE_ENUM) { + for(i=0;ivariant_count;i++) if(t->variants[i].field_count>1) { + fprintf(e->out,"struct fe_payload_%s_%s {",t->name,t->variants[i].name); + for(j=0;jvariants[i].field_count;j++) { fputs(" ",e->out); fputs(fe_type_c_name(t->variants[i].fields[j].type,e->pointer_bits),e->out); fputc(' ',e->out); fputs(t->variants[i].fields[j].name,e->out); fputc(';',e->out); } + fputs(" };\n",e->out); + } + fputs(t->cname,e->out); fputs(" { ",e->out); fputs(t->bits>8 ? "unsigned short" : "unsigned char",e->out); fputs(" tag; union { ",e->out); + for(i=0;ivariant_count;i++) { if(t->variants[i].field_count==0) fputs("unsigned char",e->out); else if(t->variants[i].field_count==1) fputs(fe_type_c_name(t->variants[i].fields[0].type,e->pointer_bits),e->out); else fprintf(e->out,"struct fe_payload_%s_%s",t->name,t->variants[i].name); fputc(' ',e->out); fputs(t->variants[i].name,e->out); fputc(';',e->out); } + fputs(" } payload; };\n",e->out); + } + t->emit_state=2; +} + +static void emit_type_defs(FeEmitter *e) +{ + FeType *t; + fputs("typedef struct { const unsigned char *p; unsigned long n; } fe_str;\n",e->out); + fputs("static fe_str fe_make_str(const unsigned char *p, unsigned long n) { fe_str s; s.p=p; s.n=n; return s; }\n",e->out); + /* Every fixed array has a slice conversion helper, even if this unit + only indexes the array. Intern those result types before emission so + their typedefs are present before helper definitions. */ + for(t=e->check->types.types;t;t=t->next) + if(t->kind==FE_TYPE_ARRAY) fe_type_slice(&e->check->types,t->elem); + for(t=e->check->types.types;t;t=t->next) emit_one_type(e,t); +} + +static void emit_type_helpers(FeEmitter *e) +{ + FeType *t; + unsigned i,j; + for(t=e->check->types.types;t;t=t->next) { + if(t->kind==FE_TYPE_STRUCT && t->maker) { + fprintf(e->out,"static %s %s(",t->cname,t->maker); + for(i=0;ifield_count;i++) { if(i) fputs(", ",e->out); fputs(fe_type_c_name(t->fields[i].type,e->pointer_bits),e->out); fprintf(e->out," p%u",i); } + fputs(") { ",e->out); fprintf(e->out,"%s v;",t->cname); + for(i=0;ifield_count;i++) fprintf(e->out," v.%s=p%u;",t->fields[i].name,i); + fputs(" return v; }\n",e->out); + } else if(t->kind==FE_TYPE_ARRAY && t->maker) { + fprintf(e->out,"static %s %s(",t->cname,t->maker); + for(i=0;ilength;i++) { if(i) fputs(", ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fprintf(e->out," p%u",i); } + fputs(") { ",e->out); fprintf(e->out,"%s v;",t->cname); + for(i=0;ilength;i++) fprintf(e->out," v.a[%u]=p%u;",i,i); + fputs(" return v; }\n",e->out); + } else if(t->kind==FE_TYPE_ENUM) { + for(i=0;ivariant_count;i++) { + FeVariantType *v=&t->variants[i]; + fprintf(e->out,"static %s %s(",t->cname,v->maker); + for(j=0;jfield_count;j++) { if(j) fputs(", ",e->out); fputs(fe_type_c_name(v->fields[j].type,e->pointer_bits),e->out); fprintf(e->out," p%u",j); } + fputs(") { ",e->out); fprintf(e->out,"%s x; x.tag=%u;",t->cname,v->tag); + for(j=0;jfield_count;j++) { if(v->field_count==1) fprintf(e->out," x.payload.%s=p%u;",v->name,j); else fprintf(e->out," x.payload.%s.%s=p%u;",v->name,v->fields[j].name,j); } + fputs(" return x; }\n",e->out); + } + } + } + for(t=e->check->types.types;t;t=t->next) { + if (t->kind==FE_TYPE_ARRAY && t->indexer) { + fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", + fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); + if(!e->no_checks) fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); + fprintf(e->out,"return x.a[i]; }\n"); + fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", + fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->slicer,t->cname); + if(!e->no_checks) fputs("if (a > b || b > ",e->out), fprintf(e->out,"%lu",t->length), fputs(") fe_trap_bounds(); ",e->out); + fprintf(e->out,"return %s(x.a+a,b-a); }\n",fe_type_slice(&e->check->types,t->elem)->maker); + fprintf(e->out,"static %s %s(%s x) { return %s(x,0,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->full_slicer,t->cname,t->slicer,t->length); + fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->tail_slicer,t->cname,t->slicer,t->length); + } else if (t->kind==FE_TYPE_SLICE && t->indexer) { + fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", + fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); + if(!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); + fputs("return x.p[i]; }\n",e->out); + fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", + fe_type_c_name(t,e->pointer_bits),t->slicer,t->cname); + if(!e->no_checks) fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); + fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker); + fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n",t->cname,t->full_slicer,t->cname,t->slicer); + fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n",t->cname,t->tail_slicer,t->cname,t->slicer); + } + } + fputs("static unsigned char fe_idx_str(fe_str x, unsigned long i) { ",e->out); + if(!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); + fputs("return x.p[i]; }\n",e->out); + fputs("static fe_str fe_slice_str(fe_str x, unsigned long a, unsigned long b) { ",e->out); + if(!e->no_checks) fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); + fputs("return fe_make_str(x.p+a,b-a); }\n",e->out); + fputs("static fe_str fe_full_slice_str(fe_str x) { return fe_slice_str(x,0,x.n); }\n",e->out); + fputs("static fe_str fe_tail_slice_str(fe_str x, unsigned long a) { return fe_slice_str(x,a,x.n); }\n",e->out); +} + static void emit_expr(FeEmitter *e, FeNode *n); static void emit_stmt(FeEmitter *e, FeNode *n); +static int stmt_definitely_returns(FeNode *n); + +static int match_is_exhaustive(FeNode *n) +{ + FeType *t; + FeNode *arm; + unsigned i; + int found; + if (!n || !n->a) return 0; + t=n->a->sem_type; + if (!t || t->kind!=FE_TYPE_ENUM) return 0; + for (arm=n->children; arm; arm=arm->next) + if (arm->text && strcmp(arm->text,"_")==0) return 1; + for (i=0; ivariant_count; ++i) { + found=0; + for (arm=n->children; arm; arm=arm->next) + if (arm->text && strcmp(arm->text,t->variants[i].name)==0) { + found=1; + break; + } + if (!found) return 0; + } + return 1; +} + +static int match_definitely_returns(FeNode *n) +{ + FeNode *arm; + if (!match_is_exhaustive(n)) return 0; + for (arm=n->children; arm; arm=arm->next) + if (!stmt_definitely_returns(arm->a)) return 0; + return 1; +} + +static int stmt_definitely_returns(FeNode *n) +{ + FeNode *last; + if (!n) return 0; + if (n->kind==FE_N_RETURN) return 1; + if (n->kind==FE_N_MATCH) return match_definitely_returns(n); + if (n->kind==FE_N_BLOCK) { + last=n->children; + if (!last) return 0; + while (last->next) last=last->next; + return stmt_definitely_returns(last); + } + if (n->kind==FE_N_IF) + return n->b && n->c && stmt_definitely_returns(n->b) && + stmt_definitely_returns(n->c); + return 0; +} + +static int hex_value(int c) +{ + if (c>='0' && c<='9') return c-'0'; + if (c>='a' && c<='f') return c-'a'+10; + if (c>='A' && c<='F') return c-'A'+10; + return -1; +} + +static void emit_byte(FILE *out, unsigned value) +{ + fprintf(out,"\\%03o",value & 255U); +} + +static void emit_codepoint(FILE *out, unsigned long cp) +{ + if (cp<=0x7fUL) emit_byte(out,(unsigned)cp); + else if (cp<=0x7ffUL) { + emit_byte(out,(unsigned)(0xc0UL | (cp>>6))); + emit_byte(out,(unsigned)(0x80UL | (cp&0x3fUL))); + } else if (cp<=0xffffUL) { + emit_byte(out,(unsigned)(0xe0UL | (cp>>12))); + emit_byte(out,(unsigned)(0x80UL | ((cp>>6)&0x3fUL))); + emit_byte(out,(unsigned)(0x80UL | (cp&0x3fUL))); + } else { + emit_byte(out,(unsigned)(0xf0UL | (cp>>18))); + emit_byte(out,(unsigned)(0x80UL | ((cp>>12)&0x3fUL))); + emit_byte(out,(unsigned)(0x80UL | ((cp>>6)&0x3fUL))); + emit_byte(out,(unsigned)(0x80UL | (cp&0x3fUL))); + } +} + +static void emit_c_literal(FILE *out, const char *text, int string) +{ + unsigned long i; + unsigned long cp; + int h0,h1,h2,h3; + int c; + char quote=string ? '"' : '\''; + if (!text) { fputs(string ? "\"\"" : "'\\000'",out); return; } + fputc(quote,out); + for(i=1;text[i] && text[i]!=quote;i++) { + c=(unsigned char)text[i]; + if(c!='\\') { + if(c==quote || c=='\\') fputc('\\',out); + fputc(c,out); + continue; + } + ++i; c=(unsigned char)text[i]; + if(c=='u' && text[i+1] && text[i+2] && text[i+3] && text[i+4]) { + h0=hex_value(text[i+1]); h1=hex_value(text[i+2]); + h2=hex_value(text[i+3]); h3=hex_value(text[i+4]); + if(h0>=0 && h1>=0 && h2>=0 && h3>=0) { + cp=(unsigned long)((h0<<12)|(h1<<8)|(h2<<4)|h3); + emit_codepoint(out,cp); i+=4; continue; + } + } + if(c=='x' && text[i+1] && text[i+2]) { + h0=hex_value(text[i+1]); h1=hex_value(text[i+2]); + if(h0>=0 && h1>=0) { emit_byte(out,(unsigned)((h0<<4)|h1)); i+=2; continue; } + } + if(c=='n') emit_byte(out,10U); + else if(c=='r') emit_byte(out,13U); + else if(c=='t') emit_byte(out,9U); + else if(c=='0') emit_byte(out,0U); + else emit_byte(out,(unsigned char)c); + } + fputc(quote,out); +} + +static void emit_lvalue(FeEmitter *e, FeNode *n) +{ + FeType *bt; + if (!n) { fputs("fe_bad_lvalue",e->out); return; } + if (n->kind==FE_N_IDENT) { fputs(cname(n,"fe_local"),e->out); return; } + if (n->kind==FE_N_MEMBER) { + if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); + } else { emit_lvalue(e,n->a); fputc('.',e->out); fputs(n->b ? n->b->text : "member",e->out); } + return; + } + if (n->kind==FE_N_INDEX) { + bt=n->a ? n->a->sem_type : 0; + emit_lvalue(e,n->a); fputs(bt && bt->kind==FE_TYPE_ARRAY ? ".a[" : ".p[",e->out); + emit_expr(e,n->b); fputc(']',e->out); return; + } + emit_expr(e,n); +} + +static FeNode *init_field(FeNode *n, const char *name) +{ + FeNode *f; + for(f=n ? n->children : 0;f;f=f->next) + if(f->kind==FE_N_FIELD && f->text && name && strcmp(f->text,name)==0) return f; + return 0; +} + +static void emit_slice_call(FeEmitter *e, FeNode *n) +{ + FeType *bt=n->a ? n->a->sem_type : 0; + const char *maker=bt && bt->slicer ? bt->slicer : "fe_slice_str"; + if (!n->b && !n->c && bt && bt->full_slicer) { + fputs(bt->full_slicer,e->out); fputc('(',e->out); emit_expr(e,n->a); fputc(')',e->out); return; + } + if (!n->c && bt && bt->tail_slicer) { + fputs(bt->tail_slicer,e->out); fputc('(',e->out); emit_expr(e,n->a); fputs(", ",e->out); + if (n->b) emit_expr(e,n->b); else fputs("0",e->out); + fputc(')',e->out); return; + } + fputs(maker,e->out); fputc('(',e->out); emit_expr(e,n->a); fputs(", ",e->out); + if(n->b) emit_expr(e,n->b); else fputs("0",e->out); + fputs(", ",e->out); + if(n->c) emit_expr(e,n->c); + else if(bt && bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); + else fputs("((unsigned long)",e->out), emit_expr(e,n->a), fputs(".n)",e->out); + fputc(')',e->out); +} + +static void emit_slice(FeEmitter *e, FeNode *n) +{ + emit_slice_call(e,n); +} + static void emit_expr(FeEmitter *e, FeNode *n) { FeNode *x; @@ -40,8 +336,43 @@ static void emit_expr(FeEmitter *e, FeNode *n) case FE_N_LITERAL: if (n->text && strcmp(n->text, "true") == 0) fputs("1", e->out); else if (n->text && strcmp(n->text, "false") == 0) fputs("0", e->out); + else if (n->text && n->text[0]=='"') { fputs("fe_make_str((const unsigned char*)",e->out); emit_c_literal(e->out,n->text,1); fputs(", sizeof(",e->out); emit_c_literal(e->out,n->text,1); fputs(")-1)",e->out); } + else if (n->text && n->text[0]=='\'') emit_c_literal(e->out,n->text,0); else fputs(n->text ? n->text : "0", e->out); break; + case FE_N_STRUCT_INIT: { + FeVariantType *v; + FeNode *f; + unsigned i; + if(n->sem_type && n->a && n->a->kind==FE_N_MEMBER) { + v=fe_type_variant(n->sem_type,n->a->b ? n->a->b->text : ""); + if(v) { fputs(v->maker,e->out); fputc('(',e->out); for(i=0;ifield_count;i++){f=init_field(n,v->fields[i].name);if(i)fputs(", ",e->out);if(f)emit_expr(e,f->a);else fputs("0",e->out);} fputc(')',e->out); } + else fputs("0",e->out); + } else if(n->sem_type && n->sem_type->maker) { + fputs(n->sem_type->maker,e->out); fputc('(',e->out); + if(n->sem_type->kind==FE_TYPE_STRUCT) { for(i=0;isem_type->field_count;i++){f=init_field(n,n->sem_type->fields[i].name);if(i)fputs(", ",e->out);if(f)emit_expr(e,f->a);else fputs("0",e->out);} } + fputc(')',e->out); + } else fputs("0",e->out); + break; + } + case FE_N_ARRAY_INIT: { + int first=1; + if(n->sem_type && n->sem_type->maker) { fputs(n->sem_type->maker,e->out); fputc('(',e->out); for(x=n->children;x;x=x->next){if(!first)fputs(", ",e->out);emit_expr(e,x);first=0;} fputc(')',e->out); } else fputs("0",e->out); + break; + } + case FE_N_INDEX: { + FeType *bt; + bt=n->a ? n->a->sem_type : 0; + if(n->c || !n->b) { + emit_slice(e,n); + } else { + if (bt && bt->indexer) { + fputs(bt->indexer,e->out); fputc('(',e->out); + emit_expr(e,n->a); fputs(", ",e->out); emit_expr(e,n->b); fputc(')',e->out); + } else fputs("0",e->out); + } + break; + } case FE_N_UNARY: op = n->text ? n->text : ""; if (strcmp(op, "not") == 0) fputs("(!", e->out); @@ -71,21 +402,35 @@ static void emit_expr(FeEmitter *e, FeNode *n) fputc(')', e->out); } else emit_expr(e, n->a); break; - case FE_N_CALL: - if (n->a) emit_expr(e, n->a); + case FE_N_CALL: { + FeVariantType *v; + int special=0; + if(!n->a && n->text && strcmp(n->text,"@size_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%lu",fe_type_size(fe_type_intern(&e->check->types,n->children->text))); special=1; } + else if(!n->a && n->text && strcmp(n->text,"@align_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%u",fe_type_align(fe_type_intern(&e->check->types,n->children->text))); special=1; } + else if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM) { + v=fe_type_variant(n->a->a->sem_type,n->a->b ? n->a->b->text : ""); + if(v) fputs(v->maker,e->out); else fputs("fe_bad_variant",e->out); + } else if (n->a) emit_expr(e, n->a); else fputs(n->text ? n->text : "fe_builtin", e->out); - fputc('(', e->out); - for (x = n->children; x; x = x->next) { - if (x != n->children) fputs(", ", e->out); - emit_expr(e, x); + if(!special) { + fputc('(', e->out); + for (x = n->children; x; x = x->next) { + if (x != n->children) fputs(", ", e->out); + emit_expr(e, x); + } + fputc(')', e->out); } - fputc(')', e->out); break; - case FE_N_MEMBER: - emit_expr(e, n->a); - fputc('.', e->out); - if (n->b) fputs(n->b->text ? n->b->text : "member", e->out); + } + case FE_N_MEMBER: { + FeVariantType *v; + if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); + } else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_ENUM) { v=fe_type_variant(n->a->sem_type,n->b ? n->b->text : ""); if(v) fputs(v->maker,e->out); else fputs("0",e->out); if(v)fputs("()",e->out); } + else { emit_expr(e, n->a); fputc('.', e->out); if (n->b) fputs(n->b->text ? n->b->text : "member", e->out); } break; + } default: fputs("0", e->out); break; @@ -125,6 +470,38 @@ static void emit_block(FeEmitter *e, FeNode *n) fputc('}', e->out); } +static void emit_match(FeEmitter *e, FeNode *n, int value_context) +{ + FeNode *arm; + FeType *t=n->a ? n->a->sem_type : 0; + FeVariantType *v; + FeNode *b; + unsigned i; + char temp[32]; + sprintf(temp,"fe_match_%u",e->temp_serial++); + pad(e); fputs("{\n",e->out); ++e->indent; + pad(e); fputs(fe_type_c_name(t,e->pointer_bits),e->out); fputc(' ',e->out); + fputs(temp,e->out); fputs(" = ",e->out); emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("switch (",e->out); fputs(temp,e->out); fputs(".tag) {\n",e->out); ++e->indent; + for(arm=n->children;arm;arm=arm->next) { + if(arm->text && strcmp(arm->text,"_")==0) { pad(e); fputs("default: ",e->out); } + else { v=t && t->kind==FE_TYPE_ENUM ? fe_type_variant(t,arm->text) : 0; if(!v) continue; fprintf(e->out,"case %u: ",v->tag); } + fputs("{\n",e->out); ++e->indent; + v=t && t->kind==FE_TYPE_ENUM ? fe_type_variant(t,arm->text) : 0; + if(v) for(i=0,b=arm->children;ifield_count && b;i++,b=b->next) { + pad(e); fputs(fe_type_c_name(v->fields[i].type,e->pointer_bits),e->out); fputc(' ',e->out); fputs(cname(b,"fe_match"),e->out); fputs(" = ",e->out); fputs(temp,e->out); fputs(".payload.",e->out); fputs(v->name,e->out); if(v->field_count>1){fputc('.',e->out);fputs(v->fields[i].name,e->out);} fputs(";\n",e->out); + } + if(arm->a && arm->a->kind==FE_N_BLOCK) emit_stmt(e,arm->a); else { pad(e); emit_expr(e,arm->a); fputs(";\n",e->out); } + pad(e); fputs("break;\n",e->out); --e->indent; pad(e); fputs("}\n",e->out); + } + --e->indent; pad(e); fputs("}\n",e->out); + --e->indent; pad(e); fputs("}\n",e->out); + if (value_context || match_definitely_returns(n)) { + pad(e); fputs("fe_trap_bounds();\n",e->out); + pad(e); fputs("return 0;\n",e->out); + } +} + static void emit_stmt(FeEmitter *e, FeNode *n) { if (!n) return; @@ -145,7 +522,7 @@ static void emit_stmt(FeEmitter *e, FeNode *n) break; case FE_N_ASSIGN: pad(e); - emit_expr(e, n->a); + emit_lvalue(e, n->a); fputc(' ', e->out); fputs(n->text ? n->text : "=", e->out); fputs(" ", e->out); @@ -159,6 +536,10 @@ static void emit_stmt(FeEmitter *e, FeNode *n) break; case FE_N_RETURN: pad(e); + if (n->a && n->a->kind == FE_N_MATCH) { + emit_match(e,n->a,1); + break; + } fputs("return", e->out); if (n->a) { fputc(' ', e->out); @@ -189,6 +570,38 @@ static void emit_stmt(FeEmitter *e, FeNode *n) else emit_block(e, 0); fputc('\n', e->out); break; + case FE_N_FOR: + pad(e); fputs("{\n",e->out); ++e->indent; + if (n->c) { + pad(e); fputs("unsigned long ",e->out); fputs(cname(n,"fe_index"),e->out); fputs(";\n",e->out); + pad(e); fputs(cname(n,"fe_index"),e->out); fputs(" = ",e->out); emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("for (; ",e->out); fputs(cname(n,"fe_index"),e->out); fputs(" < ",e->out); emit_expr(e,n->c); fputs("; ++",e->out); fputs(cname(n,"fe_index"),e->out); fputs(") ",e->out); emit_block(e,n->b); fputc('\n',e->out); + } else { + FeType *bt=n->a ? n->a->sem_type : 0; + FeType *et=bt ? bt->elem : 0; + char temp[32]; + int mutable_iter=(n->flags & 4U) != 0; + sprintf(temp,"fe_iter_%u",e->temp_serial++); + pad(e); fputs(fe_type_c_name(bt,e->pointer_bits),e->out); if (mutable_iter) fputs(" *",e->out); fputc(' ',e->out); fputs(temp,e->out); fputs(" = ",e->out); if (mutable_iter) fputc('&',e->out); emit_expr(e,n->a); fputs(";\n",e->out); + if (n->aux_text) { + pad(e); fputs("unsigned long ",e->out); fputs(cname(n,"fe_index"),e->out); fputs(";\n",e->out); + } else { + pad(e); fputs(fe_type_c_name(n->sem_type ? n->sem_type : fe_type_ref(&e->check->types,et,0),e->pointer_bits),e->out); fputc(' ',e->out); fputs(cname(n,"fe_item"),e->out); fputs(";\n",e->out); + } + pad(e); fputs("{ unsigned long fe_i; for (fe_i = 0; fe_i < ",e->out); + if(bt && bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); else { if(mutable_iter) fputs("(*",e->out); fputs(temp,e->out); if(mutable_iter) fputs(").n",e->out); else fputs(".n",e->out); } + fputs("; ++fe_i) { ",e->out); + if(n->aux_text) { + fputs(fe_type_c_name(fe_type_ref(&e->check->types,et,(n->flags & 4U) != 0),e->pointer_bits),e->out); fputc(' ',e->out); fputs(n->aux_cname ? n->aux_cname : "fe_item",e->out); fputs("; ",e->out); + fputs(cname(n,"fe_index"),e->out); fputs(" = fe_i; ",e->out); + fputs(n->aux_cname ? n->aux_cname : "fe_item",e->out); fputs(" = ",e->out); + } else { fputs(cname(n,"fe_item"),e->out); fputs(" = ",e->out); } + fputc('&',e->out); if(mutable_iter) fputs("(*",e->out); fputs(temp,e->out); if(mutable_iter) fputs(")",e->out); if(bt && bt->kind==FE_TYPE_ARRAY) fputs(".a[fe_i]",e->out); else fputs(".p[fe_i]",e->out); fputs("; ",e->out); + emit_block(e,n->b); fputs(" } }\n",e->out); + } + --e->indent; pad(e); fputs("}\n",e->out); break; + case FE_N_MATCH: + emit_match(e,n,0); break; default: break; } @@ -237,19 +650,28 @@ static void emit_main_wrapper(FeEmitter *e, FeNode *fn) } void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, - unsigned pointer_bits) + unsigned pointer_bits, int no_checks) { e->out = out; e->check = check; e->pointer_bits = pointer_bits; e->indent = 0; + e->no_checks = no_checks; + e->temp_serial = 0; } void fe_emit_c_program(FeEmitter *e) { FeNode *n; FeNode *main_fn = 0; - fputs("/* generated by fec M2 */\n#include \n\n", e->out); + fputs("/* generated by fec M3 */\n#include \n#include \ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out); + if (e->pointer_bits==16) + fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); + else + fputs("typedef char fe_assert_usize[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); + fputs("static void fe_trap_bounds(void) { abort(); }\n\n", e->out); + emit_type_defs(e); + emit_type_helpers(e); for (n = e->check->ast->root ? e->check->ast->root->children : 0; n; n = n->next) { if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { diff --git a/fec/src/emit_c.h b/fec/src/emit_c.h index 0d2aa29..b9f12ab 100644 --- a/fec/src/emit_c.h +++ b/fec/src/emit_c.h @@ -8,9 +8,12 @@ typedef struct FeEmitter { FeCheck *check; unsigned pointer_bits; int indent; + int no_checks; + unsigned temp_serial; } FeEmitter; -void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, unsigned pointer_bits); +void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, + unsigned pointer_bits, int no_checks); void fe_emit_c_program(FeEmitter *e); #endif diff --git a/fec/src/parser.c b/fec/src/parser.c index 434befa..f5abc90 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -11,13 +11,14 @@ static int want(FeParser *p, FeTokKind k, const char *what) { if(eat(p,k)) return 1; error(p,what); return 0; } static int is_name(FeParser *p) { return is(p,FE_TOK_IDENT)||is(p,FE_TOK_SELF)||is(p,FE_TOK_SELFTYPE); } static FeNode *expr(FeParser *p, int minprec); +static FeNode *delimited_expr(FeParser *p); static FeNode *type(FeParser *p); static FeNode *statement(FeParser *p); static FeNode *block(FeParser *p); void fe_parser_init(FeParser *p, FeAst *ast, const char *src, unsigned long length, const char *file, FeDiags *d) { - p->ast=ast; p->diags=d; fe_lexer_init(&p->lexer,src,length,file,d); + p->ast=ast; p->diags=d; p->forbid_struct_literal=0; fe_lexer_init(&p->lexer,src,length,file,d); p->previous=p->current=fe_lexer_next(&p->lexer); } @@ -81,10 +82,19 @@ static int precedence(FeTokKind k) static FeNode *primary(FeParser *p) { FeToken t=p->current; FeNode *n; + if (is(p,FE_TOK_LBRACKET)) { + FeNode *a=toknode(p,FE_N_ARRAY_INIT,t); next(p); + while(!is(p,FE_TOK_RBRACKET)&&!is(p,FE_TOK_EOF)) { + fe_node_add(a,delimited_expr(p)); + if(!eat(p,FE_TOK_COMMA)) break; + } + want(p,FE_TOK_RBRACKET,"expected ']' after array literal"); + return a; + } if(is(p,FE_TOK_INT)||is(p,FE_TOK_CHAR)||is(p,FE_TOK_STRING)||is(p,FE_TOK_TRUE)||is(p,FE_TOK_FALSE)||is(p,FE_TOK_NULL)||is(p,FE_TOK_UNDEFINED)) {next(p);return toknode(p,FE_N_LITERAL,t);} if(is_name(p) || is(p,FE_TOK_ERROR_KW)) { next(p); n=toknode(p,FE_N_IDENT,t); - if(is(p,FE_TOK_LBRACE)) { + if(is(p,FE_TOK_LBRACE) && !p->forbid_struct_literal) { FeNode *s=toknode(p,FE_N_STRUCT_INIT,t); next(p); while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { FeNode *f; if(!is_name(p)){error(p,"expected field name");recover(p);break;} f=toknode(p,FE_N_FIELD,p->current);next(p);want(p,FE_TOK_COLON,"expected ':' after field");f->a=expr(p,0);fe_node_add(s,f);if(!eat(p,FE_TOK_COMMA))break; @@ -92,11 +102,11 @@ static FeNode *primary(FeParser *p) } return n; } - if(eat(p,FE_TOK_LPAREN)) { n=expr(p,0); want(p,FE_TOK_RPAREN,"expected ')'"); return n; } + if(eat(p,FE_TOK_LPAREN)) { int old=p->forbid_struct_literal; p->forbid_struct_literal=0; n=expr(p,0); p->forbid_struct_literal=old; want(p,FE_TOK_RPAREN,"expected ')'"); return n; } if(eat(p,FE_TOK_AT)) { FeToken name=p->current; if(!is_name(p)){error(p,"expected builtin name after '@'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"builtin",7);} next(p); n=toknode(p,FE_N_CALL,name); n->text=fe_arena_strdup(&p->ast->arena,name.begin-1,name.length+1); - if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,expr(p,0));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after builtin");} + if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,delimited_expr(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after builtin");} return n; } error(p,"expected expression"); next(p); return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"expression",10); @@ -106,9 +116,29 @@ static FeNode *postfix(FeParser *p) FeNode *n=primary(p); for(;;) { FeToken t=p->current; FeNode *m; - if(eat(p,FE_TOK_LPAREN)) { m=toknode(p,FE_N_CALL,t); m->a=n; while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(m,expr(p,0));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' after call"); n=m; } - else if(eat(p,FE_TOK_LBRACKET)) { m=toknode(p,FE_N_INDEX,t);m->a=n;m->b=expr(p,0);if(eat(p,FE_TOK_DOTDOT)){m->c=expr(p,0);}want(p,FE_TOK_RBRACKET,"expected ']' after index");n=m; } - else if(eat(p,FE_TOK_DOT)) { m=toknode(p,FE_N_MEMBER,t);m->a=n;if(is_name(p)){m->b=toknode(p,FE_N_IDENT,p->current);next(p);}else if(eat(p,FE_TOK_QUESTION)){m->text=fe_arena_strdup(&p->ast->arena,".?",2);}else error(p,"expected member name");n=m; } + if(eat(p,FE_TOK_LPAREN)) { m=toknode(p,FE_N_CALL,t); m->a=n; while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(m,delimited_expr(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' after call"); n=m; } + else if(eat(p,FE_TOK_LBRACKET)) { + m=toknode(p,FE_N_INDEX,t);m->a=n; + if(is(p,FE_TOK_DOTDOT)) m->b=0; else m->b=delimited_expr(p); + if(eat(p,FE_TOK_DOTDOT)) { if(!is(p,FE_TOK_RBRACKET)) m->c=delimited_expr(p); } + want(p,FE_TOK_RBRACKET,"expected ']' after index");n=m; + } + else if(eat(p,FE_TOK_DOT)) { + m=toknode(p,FE_N_MEMBER,t);m->a=n; + if(is_name(p)){m->b=toknode(p,FE_N_IDENT,p->current);next(p);} + else if(eat(p,FE_TOK_QUESTION)){m->text=fe_arena_strdup(&p->ast->arena,".?",2);} + else if(eat(p,FE_TOK_XOR)){m->text=fe_arena_strdup(&p->ast->arena,".^",2);m->b=fe_node(p->ast,FE_N_IDENT,p->previous.loc,"^",1);} + else error(p,"expected member name"); + n=m; + if(is(p,FE_TOK_LBRACE) && !p->forbid_struct_literal) { + FeNode *s=toknode(p,FE_N_STRUCT_INIT,t); s->a=n; next(p); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { FeNode *f; + if(!is_name(p)){error(p,"expected variant field");recover(p);break;} + f=toknode(p,FE_N_FIELD,p->current);next(p);want(p,FE_TOK_COLON,"expected ':' after variant field");f->a=expr(p,0);fe_node_add(s,f);if(!eat(p,FE_TOK_COMMA))break; + } + want(p,FE_TOK_RBRACE,"expected '}' in variant constructor");n=s; + } + } else if(eat(p,FE_TOK_AS)) { m=toknode(p,FE_N_TYPE,t);m->a=n;m->b=type(p);n=m; } else break; } @@ -123,6 +153,29 @@ static FeNode *expr(FeParser *p, int minprec) return left; } +/* A control-flow header is followed by a body '{'. Do not let that body + brace be consumed as the postfix struct-literal brace; callers can use + parentheses when a struct literal is intended in the header. */ +static FeNode *header_expr(FeParser *p) +{ + FeNode *n; + int old=p->forbid_struct_literal; + p->forbid_struct_literal=1; + n=expr(p,0); + p->forbid_struct_literal=old; + return n; +} + +static FeNode *delimited_expr(FeParser *p) +{ + FeNode *n; + int old=p->forbid_struct_literal; + p->forbid_struct_literal=0; + n=expr(p,0); + p->forbid_struct_literal=old; + return n; +} + static FeNode *params(FeParser *p) { FeNode *list=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"params",6); @@ -147,7 +200,7 @@ static FeNode *field(FeParser *p) } static FeNode *decl(FeParser *p) { - int pub=0, external=0, interrupt=0, interrupt_safe=0, shared=0, atomic=0; FeToken t=p->current; FeNode *n; + int pub=0, external=0, interrupt=0, interrupt_safe=0, shared=0, atomic=0; FeToken t=p->current; FeNode *n; FeTokKind before; (void)shared; (void)atomic; if(eat(p,FE_TOK_PUB)) pub=1; if(eat(p,FE_TOK_EXTERN)) { external=1; if(is(p,FE_TOK_STRING)) next(p); } @@ -156,12 +209,14 @@ static FeNode *decl(FeParser *p) if(!is(p,FE_TOK_PACKED)) t=p->current; if(is(p,FE_TOK_FN)) return fn_decl(p,pub,external,interrupt,interrupt_safe); if(eat(p,FE_TOK_PACKED)) t=p->previous; - if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){if(is(p,FE_TOK_PUB))next(p);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,0,0,0,0));else fe_node_add(n,field(p));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } + if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(t.kind==FE_TOK_PACKED)n->flags|=1U;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){if(is(p,FE_TOK_PUB))next(p);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,0,0,0,0));else fe_node_add(n,field(p));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } - error(p,"expected declaration"); recover(p); return 0; + error(p,"expected declaration"); before=p->current.kind; recover(p); + if (p->current.kind==before && p->current.kind!=FE_TOK_EOF) next(p); + return 0; } static FeNode *block(FeParser *p) @@ -175,11 +230,42 @@ static FeNode *statement(FeParser *p) if(eat(p,FE_TOK_LET)) { n=toknode(p,FE_N_LET,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in let");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_VAR)) { n=toknode(p,FE_N_VAR,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);if(eat(p,FE_TOK_EQ))n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_CONST)) { n=toknode(p,FE_N_CONST,t);if(is_name(p))next(p);else error(p,"expected constant name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in const");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } - if(eat(p,FE_TOK_IF)) { n=toknode(p,FE_N_IF,t);if(eat(p,FE_TOK_LET)){n->text=fe_arena_strdup(&p->ast->arena,"if let",6);if(is_name(p))next(p);if(eat(p,FE_TOK_LPAREN)){if(is_name(p))next(p);want(p,FE_TOK_RPAREN,"expected ')' in if let pattern");}want(p,FE_TOK_EQ,"expected '=' in if let");}n->a=expr(p,0);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } - if(eat(p,FE_TOK_COMPTIME)) { n=toknode(p,FE_N_IF,t);want(p,FE_TOK_IF,"expected 'if' after comptime");n->text=fe_arena_strdup(&p->ast->arena,"comptime if",11);n->a=expr(p,0);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } - if(eat(p,FE_TOK_WHILE)) {n=toknode(p,FE_N_WHILE,t);n->a=expr(p,0);n->b=block(p);return n;} - if(eat(p,FE_TOK_FOR)) {n=toknode(p,FE_N_FOR,t);if(is_name(p))next(p);else error(p,"expected loop variable");if(eat(p,FE_TOK_COMMA)){if(is_name(p))next(p);else error(p,"expected second loop variable");}want(p,FE_TOK_IN,"expected 'in' in for");n->a=expr(p,0);if(eat(p,FE_TOK_DOTDOT))n->c=expr(p,0);n->b=block(p);return n;} - if(eat(p,FE_TOK_MATCH)) { n=toknode(p,FE_N_MATCH,t);n->a=expr(p,0);want(p,FE_TOK_LBRACE,"expected '{' after match expression");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *arm=toknode(p,FE_N_ARM,p->current);if(is_name(p)||is(p,FE_TOK_INT)||is(p,FE_TOK_CHAR)||is(p,FE_TOK_NULL)||is(p,FE_TOK_TRUE)||is(p,FE_TOK_FALSE)||is(p,FE_TOK_IDENT)){arm->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else{error(p,"expected match pattern");recover(p);continue;}while(is(p,FE_TOK_LPAREN)||is(p,FE_TOK_LBRACE)){FeTokKind close=is(p,FE_TOK_LPAREN)?FE_TOK_RPAREN:FE_TOK_RBRACE;next(p);while(!is(p,close)&&!is(p,FE_TOK_EOF))next(p);want(p,close,"expected end of match pattern");}want(p,FE_TOK_FATARROW,"expected '=>' in match arm");if(is(p,FE_TOK_LBRACE))arm->a=block(p);else{arm->a=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' in match arm");}fe_node_add(n,arm);}want(p,FE_TOK_RBRACE,"expected '}' after match");return n;} + if(eat(p,FE_TOK_IF)) { n=toknode(p,FE_N_IF,t);if(eat(p,FE_TOK_LET)){n->text=fe_arena_strdup(&p->ast->arena,"if let",6);if(is_name(p))next(p);if(eat(p,FE_TOK_LPAREN)){if(is_name(p))next(p);want(p,FE_TOK_RPAREN,"expected ')' in if let pattern");}want(p,FE_TOK_EQ,"expected '=' in if let");}n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } + if(eat(p,FE_TOK_COMPTIME)) { n=toknode(p,FE_N_IF,t);want(p,FE_TOK_IF,"expected 'if' after comptime");n->text=fe_arena_strdup(&p->ast->arena,"comptime if",11);n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } + if(eat(p,FE_TOK_WHILE)) {n=toknode(p,FE_N_WHILE,t);n->a=header_expr(p);n->b=block(p);return n;} + if(eat(p,FE_TOK_FOR)) {n=toknode(p,FE_N_FOR,t);if(is_name(p)){n->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected loop variable");if(eat(p,FE_TOK_COMMA)){if(is_name(p)){n->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected second loop variable");}want(p,FE_TOK_IN,"expected 'in' in for");n->a=header_expr(p);if(eat(p,FE_TOK_DOTDOT))n->c=header_expr(p);n->b=block(p);return n;} + if(eat(p,FE_TOK_MATCH)) { + int old=p->forbid_struct_literal; + n=toknode(p,FE_N_MATCH,t); p->forbid_struct_literal=1; n->a=header_expr(p); p->forbid_struct_literal=old; + want(p,FE_TOK_LBRACE,"expected '{' after match expression"); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { + FeNode *arm=toknode(p,FE_N_ARM,p->current); + FeToken pt=p->current; + if(is_name(p)||is(p,FE_TOK_INT)||is(p,FE_TOK_CHAR)||is(p,FE_TOK_NULL)||is(p,FE_TOK_TRUE)||is(p,FE_TOK_FALSE)) { + arm->text=fe_arena_strdup(&p->ast->arena,pt.begin,pt.length); next(p); + } else { error(p,"expected match pattern"); recover(p); continue; } + if(eat(p,FE_TOK_LPAREN)) { + while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { + if(is_name(p)) { fe_node_add(arm,toknode(p,FE_N_IDENT,p->current)); next(p); } + else { error(p,"expected pattern binding"); recover(p); break; } + if(!eat(p,FE_TOK_COMMA)) break; + } + want(p,FE_TOK_RPAREN,"expected ')' after match pattern"); + } else if(eat(p,FE_TOK_LBRACE)) { + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { + if(is_name(p)) { fe_node_add(arm,toknode(p,FE_N_IDENT,p->current)); next(p); } + else { error(p,"expected field binding"); recover(p); break; } + if(!eat(p,FE_TOK_COMMA)) break; + } + want(p,FE_TOK_RBRACE,"expected '}' after match pattern"); + } + want(p,FE_TOK_FATARROW,"expected '=>' in match arm"); + if(is(p,FE_TOK_LBRACE)) arm->a=block(p); + else { arm->a=expr(p,0); want(p,FE_TOK_SEMI,"expected ';' in match arm"); } + fe_node_add(n,arm); + } + want(p,FE_TOK_RBRACE,"expected '}' after match"); return n; + } if(eat(p,FE_TOK_RETURN)) {n=toknode(p,FE_N_RETURN,t);if(!is(p,FE_TOK_SEMI))n->a=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after return");return n;} if(eat(p,FE_TOK_BREAK)){n=toknode(p,FE_N_BREAK,t);want(p,FE_TOK_SEMI,"expected ';'");return n;} if(eat(p,FE_TOK_CONTINUE)){n=toknode(p,FE_N_CONTINUE,t);want(p,FE_TOK_SEMI,"expected ';'");return n;} diff --git a/fec/src/parser.h b/fec/src/parser.h index a0d5ef7..8b6f5fe 100644 --- a/fec/src/parser.h +++ b/fec/src/parser.h @@ -9,6 +9,7 @@ typedef struct FeParser { FeToken previous; FeAst *ast; FeDiags *diags; + int forbid_struct_literal; } FeParser; void fe_parser_init(FeParser *p, FeAst *ast, const char *src, unsigned long length, const char *file, FeDiags *d); diff --git a/fec/src/types.c b/fec/src/types.c index 37a17d0..8640ac1 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -1,50 +1,473 @@ #include "types.h" #include +#include +#include + +static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) +{ + FeType *t; + unsigned i; + t = (FeType *)fe_arena_alloc(ctx->arena, sizeof(FeType)); + if (!t) return 0; + for (i = 0; i + 1U < sizeof(t->name) && name && name[i]; ++i) + t->name[i] = name[i]; + t->name[i] = '\0'; + t->kind = kind; + t->cname = 0; + t->maker = 0; + t->indexer = 0; + t->slicer = 0; + t->full_slicer = 0; + t->tail_slicer = 0; + t->bits = 0; + t->is_unsigned = 0; + t->packed = 0; + t->length = 0; + t->size = 0; + t->align = 1; + t->elem = 0; + t->ref_mut = 0; + t->fields = 0; + t->field_count = 0; + t->variants = 0; + t->variant_count = 0; + t->next = ctx->types; + t->emit_state = 0; + t->cycle_state = 0; + ctx->types = t; + return t; +} void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits) -{ ctx->arena=arena; ctx->types=0; ctx->pointer_bits=pointer_bits; } +{ + ctx->arena = arena; + ctx->types = 0; + ctx->pointer_bits = pointer_bits; + ctx->unit_name = "unit"; + ctx->generated_serial = 0; +} FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) { - FeType *t; unsigned i; unsigned bits=0; int uns=0; FeTypeKind kind=FE_TYPE_UNKNOWN; - if (!name) name=""; - for (t=ctx->types;t;t=t->next) if(strcmp(t->name,name)==0) return t; - if(strcmp(name,"void")==0) kind=FE_TYPE_VOID; - else if(strcmp(name,"bool")==0) kind=FE_TYPE_BOOL; - else if(strcmp(name,"i8")==0||strcmp(name,"u8")==0){kind=FE_TYPE_INT;bits=8;uns=name[0]=='u';} - else if(strcmp(name,"i16")==0||strcmp(name,"u16")==0){kind=FE_TYPE_INT;bits=16;uns=name[0]=='u';} - else if(strcmp(name,"i32")==0||strcmp(name,"u32")==0){kind=FE_TYPE_INT;bits=32;uns=name[0]=='u';} - else if(strcmp(name,"usize")==0||strcmp(name,"isize")==0){kind=FE_TYPE_INT;bits=ctx->pointer_bits;uns=name[0]=='u';} - t=(FeType *)fe_arena_alloc(ctx->arena,sizeof(FeType)); if(!t)return 0; - for(i=0;iname)-1 && name[i];i++) t->name[i]=name[i]; - t->name[i]='\0'; - t->kind=kind;t->bits=bits;t->is_unsigned=uns;t->next=ctx->types;ctx->types=t;return t; + FeType *t; + unsigned bits = 0; + int uns = 0; + FeTypeKind kind = FE_TYPE_UNKNOWN; + if (!name) name = ""; + for (t = ctx->types; t; t = t->next) + if (strcmp(t->name, name) == 0) return t; + if (strcmp(name, "void") == 0) kind = FE_TYPE_VOID; + else if (strcmp(name, "bool") == 0) kind = FE_TYPE_BOOL; + else if (strcmp(name, "char") == 0) kind = FE_TYPE_CHAR; + else if (strcmp(name, "str") == 0) kind = FE_TYPE_STR; + else if (strcmp(name, "i8") == 0 || strcmp(name, "u8") == 0) { + kind = FE_TYPE_INT; bits = 8; uns = name[0] == 'u'; + } else if (strcmp(name, "i16") == 0 || strcmp(name, "u16") == 0) { + kind = FE_TYPE_INT; bits = 16; uns = name[0] == 'u'; + } else if (strcmp(name, "i32") == 0 || strcmp(name, "u32") == 0) { + kind = FE_TYPE_INT; bits = 32; uns = name[0] == 'u'; + } else if (strcmp(name, "usize") == 0 || strcmp(name, "isize") == 0) { + kind = FE_TYPE_INT; bits = ctx->pointer_bits; uns = name[0] == 'u'; + } + t = new_type(ctx, name, kind); + if (!t) return 0; + t->bits = bits; + t->is_unsigned = uns; + if (kind == FE_TYPE_STR) { + t->cname = fe_arena_strdup(ctx->arena, "fe_str", 6); + t->elem = fe_type_intern(ctx, "u8"); + t->indexer = "fe_idx_str"; + t->slicer = "fe_slice_str"; + t->full_slicer = "fe_full_slice_str"; + t->tail_slicer = "fe_tail_slice_str"; + } + return t; +} + +static char *generated_name(FeTypeCtx *ctx, const char *prefix, + const char *name) +{ + char number[24]; + unsigned long n; + char *p; + sprintf(number, "%u", ctx->generated_serial++); + n = (unsigned long)strlen(prefix) + (unsigned long)strlen(name) + + (unsigned long)strlen(number) + 2UL; + p = (char *)fe_arena_alloc(ctx->arena, n); + if (!p) return 0; + strcpy(p, prefix); + strcat(p, name); + strcat(p, "_"); + strcat(p, number); + return p; +} + +FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem) +{ + char key[96]; + FeType *t; + sprintf(key, "[%lu]%s", length, elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_ARRAY; + t->length = length; + t->elem = elem; + t->cname = generated_name(ctx, "struct fe_arr_", "type"); + t->maker = generated_name(ctx, "fe_make_arr_", "type"); + t->indexer = generated_name(ctx, "fe_idx_arr_", "type"); + t->slicer = generated_name(ctx, "fe_slice_arr_", "type"); + t->full_slicer = generated_name(ctx, "fe_full_arr_", "type"); + t->tail_slicer = generated_name(ctx, "fe_tail_arr_", "type"); + } + return t; +} + +FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem) +{ + char key[96]; + FeType *t; + sprintf(key, "[]%s", elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_SLICE; + t->elem = elem; + t->cname = generated_name(ctx, "fe_slice_", "type"); + t->maker = generated_name(ctx, "fe_make_slice_", "type"); + t->indexer = generated_name(ctx, "fe_idx_slice_", "type"); + t->slicer = generated_name(ctx, "fe_slice_slice_", "type"); + t->full_slicer = generated_name(ctx, "fe_full_slice_", "type"); + t->tail_slicer = generated_name(ctx, "fe_tail_slice_", "type"); + } + return t; +} + +FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) +{ + char key[128]; + FeType *t; + sprintf(key,"%s%s",mutable ? "&mut " : "&",elem ? elem->name : "?"); + t=fe_type_intern(ctx,key); + if(t->kind==FE_TYPE_UNKNOWN) { + t->kind=FE_TYPE_REF; + t->elem=elem; + t->ref_mut=mutable; + } + return t; +} + +FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) +{ + FeType *t; + FeNode *f; + unsigned count = 0; + unsigned i = 0; + char *cname; + if (!node || !node->text) return 0; + t = fe_type_intern(ctx, node->text); + if (t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_STRUCT) return t; + if (t->kind == FE_TYPE_STRUCT) return t; + t->kind = FE_TYPE_STRUCT; + t->packed = packed; + cname = (char *)fe_arena_alloc(ctx->arena, + (unsigned long)strlen("struct fe_") + strlen(ctx->unit_name) + + strlen(node->text) + 2UL); + if (!cname) return t; + strcpy(cname, "struct fe_"); + strcat(cname, ctx->unit_name); + strcat(cname, "_"); + strcat(cname, node->text); + t->cname = cname; + t->maker = generated_name(ctx, "fe_make_", node->text); + for (f = node->children; f; f = f->next) + if (f->kind == FE_N_FIELD) ++count; + t->field_count = count; + if (count) { + t->fields = (FeFieldType *)fe_arena_alloc(ctx->arena, + count * sizeof(FeFieldType)); + if (!t->fields) return t; + for (f = node->children; f; f = f->next) if (f->kind == FE_N_FIELD) { + t->fields[i].name = f->text; + t->fields[i].type = 0; + t->fields[i].offset = 0; + t->fields[i].ast_node = f; + ++i; + } + } + return t; +} + +FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node) +{ + FeType *t; + FeNode *v; + unsigned count = 0; + unsigned i = 0; + char *cname; + if (!node || !node->text) return 0; + t = fe_type_intern(ctx, node->text); + if (t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ENUM) return t; + if (t->kind == FE_TYPE_ENUM) return t; + t->kind = FE_TYPE_ENUM; + cname = (char *)fe_arena_alloc(ctx->arena, + (unsigned long)strlen("struct fe_") + strlen(ctx->unit_name) + + strlen(node->text) + 2UL); + if (!cname) return t; + strcpy(cname, "struct fe_"); + strcat(cname, ctx->unit_name); + strcat(cname, "_"); + strcat(cname, node->text); + t->cname = cname; + for (v = node->children; v; v = v->next) ++count; + t->variant_count = count; + if (count) { + t->variants = (FeVariantType *)fe_arena_alloc(ctx->arena, + count * sizeof(FeVariantType)); + if (!t->variants) return t; + for (v = node->children; v; v = v->next) { + t->variants[i].name = v->text; + t->variants[i].fields = 0; + t->variants[i].field_count = 0; + t->variants[i].tag = i; + t->variants[i].ast_node = v; + t->variants[i].maker = generated_name(ctx, "fe_make_variant_", v->text ? v->text : "variant"); + if (v->a && v->a->kind == FE_N_TYPE) { + t->variants[i].field_count = 1; + t->variants[i].fields = (FeFieldType *)fe_arena_alloc(ctx->arena, sizeof(FeFieldType)); + if (t->variants[i].fields) { + t->variants[i].fields[0].name = "value"; + t->variants[i].fields[0].type = fe_type_from_ast(ctx, v->a); + t->variants[i].fields[0].offset = 0; + t->variants[i].fields[0].ast_node = v; + } + } + if (!v->a) { + FeNode *f; + unsigned fc = 0; + unsigned j = 0; + for (f = v->children; f; f = f->next) + if (f->kind == FE_N_FIELD) ++fc; + t->variants[i].field_count = fc; + if (fc) { + t->variants[i].fields = (FeFieldType *)fe_arena_alloc( + ctx->arena, fc * sizeof(FeFieldType)); + if (t->variants[i].fields) for (f = v->children; f; f = f->next) + if (f->kind == FE_N_FIELD) { + t->variants[i].fields[j].name = f->text; + t->variants[i].fields[j].type = 0; + t->variants[i].fields[j].offset = 0; + t->variants[i].fields[j].ast_node = f; + ++j; + } + } + } + ++i; + } + } + return t; +} + +static unsigned long round_up(unsigned long x, unsigned a) +{ + unsigned long rem; + if (a <= 1U) return x; + rem = x % (unsigned long)a; + return rem ? x + (unsigned long)a - rem : x; +} + +unsigned long fe_type_size(const FeType *t) +{ + return t ? t->size : 0; +} + +unsigned fe_type_align(const FeType *t) +{ + return t && t->align ? t->align : 1U; +} + +static void layout_type(FeTypeCtx *ctx, FeType *t) +{ + unsigned i; + unsigned align; + unsigned long off; + unsigned long max_size; + unsigned max_align; + if (!t || t->size) return; + if (t->cycle_state == 1) { + /* The checker reports this as an invalid by-value cycle. Give the + layout walk a sentinel size so error recovery cannot recurse. */ + t->size = 1; + t->align = 1; + return; + } + t->cycle_state = 1; + if (t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN || + t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->cycle_state = 2; return; } + if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) { + t->size = 1; t->align = 1; t->cycle_state = 2; return; + } + if (t->kind == FE_TYPE_INT) { + t->size = (t->bits + 7U) / 8U; + t->align = ctx->pointer_bits == 16 ? 1U : t->size; + if (t->size > 4UL) t->size = ctx->pointer_bits == 16 ? 2UL : 4UL; + t->cycle_state = 2; return; + } + if (t->kind == FE_TYPE_REF) { + t->size = ctx->pointer_bits == 16 ? 2UL : 4UL; + t->align = ctx->pointer_bits == 16 ? 1U : 4U; + t->cycle_state = 2; return; + } + if (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR) { + t->size = ctx->pointer_bits == 16 ? 4UL : 8UL; + t->align = ctx->pointer_bits == 16 ? 1U : 4U; + t->cycle_state = 2; return; + } + if (t->kind == FE_TYPE_ARRAY) { + layout_type(ctx, t->elem); + t->align = t->packed || ctx->pointer_bits == 16 ? 1U : fe_type_align(t->elem); + t->size = t->length * fe_type_size(t->elem); + t->cycle_state = 2; return; + } + if (t->kind == FE_TYPE_STRUCT) { + off = 0; max_align = 1; + for (i = 0; i < t->field_count; ++i) { + if (!t->fields[i].type && t->fields[i].ast_node) + t->fields[i].type = fe_type_from_ast(ctx, t->fields[i].ast_node->a); + layout_type(ctx, t->fields[i].type); + align = t->packed || ctx->pointer_bits == 16 ? 1U : fe_type_align(t->fields[i].type); + if (align > max_align) max_align = align; + off = round_up(off, align); + t->fields[i].offset = off; + off += fe_type_size(t->fields[i].type); + } + t->align = max_align; + t->size = round_up(off, max_align); + t->cycle_state = 2; + return; + } + if (t->kind == FE_TYPE_ENUM) { + max_size = 0; max_align = 1; + for (i = 0; i < t->variant_count; ++i) { + unsigned j; + off = 0; + for (j = 0; j < t->variants[i].field_count; ++j) { + if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) + t->variants[i].fields[j].type = fe_type_from_ast( + ctx, t->variants[i].fields[j].ast_node->a); + layout_type(ctx, t->variants[i].fields[j].type); + if (fe_type_align(t->variants[i].fields[j].type) > max_align) + max_align = fe_type_align(t->variants[i].fields[j].type); + off += fe_type_size(t->variants[i].fields[j].type); + } + if (off > max_size) max_size = off; + } + t->bits = t->variant_count > 256U ? 16U : 8U; + off = ctx->pointer_bits == 16 ? t->bits / 8U : round_up(t->bits / 8U, max_align); + t->size = round_up(off + max_size, ctx->pointer_bits == 16 ? 1U : max_align); + t->align = ctx->pointer_bits == 16 ? 1U : max_align; + t->cycle_state = 2; + } +} + +void fe_type_layout_all(FeTypeCtx *ctx) +{ + FeType *t; + for (t = ctx->types; t; t = t->next) layout_type(ctx, t); +} + +FeFieldType *fe_type_field(FeType *t, const char *name) +{ + unsigned i; + if (!t || t->kind != FE_TYPE_STRUCT || !name) return 0; + for (i = 0; i < t->field_count; ++i) + if (strcmp(t->fields[i].name, name) == 0) return &t->fields[i]; + return 0; +} + +FeVariantType *fe_type_variant(FeType *t, const char *name) +{ + unsigned i; + if (!t || t->kind != FE_TYPE_ENUM || !name) return 0; + for (i = 0; i < t->variant_count; ++i) + if (strcmp(t->variants[i].name, name) == 0) return &t->variants[i]; + return 0; } FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) { - if(!node)return fe_type_intern(ctx,""); - if(node->kind!=FE_N_TYPE)return fe_type_intern(ctx,""); - if(node->text && (strcmp(node->text,"?")==0||strcmp(node->text,"!")==0||strcmp(node->text,"^")==0||strcmp(node->text,"&")==0||strcmp(node->text,"*")==0||strcmp(node->text,"far")==0))return fe_type_intern(ctx,""); - if(node->text && strcmp(node->text,"as")==0)return fe_type_from_ast(ctx,node->b); - if(node->text && strcmp(node->text,"fn")==0)return fe_type_intern(ctx,""); - return fe_type_intern(ctx,node->text); + unsigned long length = 0; + if (!node) return fe_type_intern(ctx, ""); + if (node->kind != FE_N_TYPE) return fe_type_intern(ctx, ""); + if (node->text && strcmp(node->text, "as") == 0) + return fe_type_from_ast(ctx, node->b); + if (node->text && strcmp(node->text, "str") == 0) + return fe_type_intern(ctx, "str"); + if (node->text && (strcmp(node->text, "&") == 0 || + strcmp(node->text, "&mut") == 0)) + return fe_type_ref(ctx, fe_type_from_ast(ctx,node->a), + strcmp(node->text,"&mut") == 0); + if (node->text && strcmp(node->text, "[") == 0) { + if (node->a) { + if (node->a->kind == FE_N_LITERAL && node->a->text) + length = strtoul(node->a->text, 0, 0); + return fe_type_array(ctx, length, fe_type_from_ast(ctx, node->b)); + } + return fe_type_slice(ctx, fe_type_from_ast(ctx, node->b)); + } + if (node->text && (strcmp(node->text, "?") == 0 || + strcmp(node->text, "!") == 0 || + strcmp(node->text, "^") == 0 || + strcmp(node->text, "&") == 0 || + strcmp(node->text, "&mut") == 0 || + strcmp(node->text, "*") == 0 || + strcmp(node->text, "far") == 0)) + return fe_type_intern(ctx, ""); + if (node->text && strcmp(node->text, "fn") == 0) + return fe_type_intern(ctx, ""); + return fe_type_intern(ctx, node->text); } -int fe_type_equal(const FeType *a,const FeType *b){return a==b || (a&&b&&strcmp(a->name,b->name)==0);} -int fe_type_is_integer(const FeType *t){return t&&t->kind==FE_TYPE_INT;} -const char *fe_type_c_name(const FeType *t,unsigned pointer_bits) + +int fe_type_equal(const FeType *a, const FeType *b) { - if(!t)return "int"; - if(t->kind==FE_TYPE_VOID)return "void"; - if(t->kind==FE_TYPE_BOOL)return "unsigned char"; - if(t->kind!=FE_TYPE_INT)return "int"; - if(strcmp(t->name,"usize")==0)return pointer_bits==16?"unsigned short":"unsigned long"; - if(strcmp(t->name,"isize")==0)return pointer_bits==16?"short":"long"; - if(strcmp(t->name,"i8")==0)return "signed char"; - if(strcmp(t->name,"u8")==0)return "unsigned char"; - if(strcmp(t->name,"i16")==0)return "short"; - if(strcmp(t->name,"u16")==0)return "unsigned short"; - if(strcmp(t->name,"i32")==0)return "long"; - if(strcmp(t->name,"u32")==0)return "unsigned long"; - return "int"; + return a == b || (a && b && strcmp(a->name, b->name) == 0); +} + +int fe_type_is_integer(const FeType *t) +{ + return t && t->kind == FE_TYPE_INT; +} + +int fe_type_is_indexable(const FeType *t) +{ + return t && (t->kind == FE_TYPE_ARRAY || t->kind == FE_TYPE_SLICE || + t->kind == FE_TYPE_STR); +} + +const char *fe_type_c_name(const FeType *t, unsigned pointer_bits) +{ + if (!t) return "long"; + if (t->cname) return t->cname; + if (t->kind == FE_TYPE_VOID) return "void"; + if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) return "unsigned char"; + if (t->kind == FE_TYPE_REF) { + static char ref_name[128]; + if (t->ref_mut) { + strcpy(ref_name,fe_type_c_name(t->elem,pointer_bits)); + strcat(ref_name," *"); + } else { + strcpy(ref_name,"const "); + strcat(ref_name,fe_type_c_name(t->elem,pointer_bits)); + strcat(ref_name," *"); + } + return ref_name; + } + if (t->kind != FE_TYPE_INT) return "long"; + if (strcmp(t->name, "usize") == 0) return pointer_bits == 16 ? "unsigned short" : "unsigned long"; + if (strcmp(t->name, "isize") == 0) return pointer_bits == 16 ? "short" : "long"; + if (strcmp(t->name, "i8") == 0) return "signed char"; + if (strcmp(t->name, "u8") == 0) return "unsigned char"; + if (strcmp(t->name, "i16") == 0) return "short"; + if (strcmp(t->name, "u16") == 0) return "unsigned short"; + if (strcmp(t->name, "i32") == 0) return "long"; + if (strcmp(t->name, "u32") == 0) return "unsigned long"; + return "long"; } diff --git a/fec/src/types.h b/fec/src/types.h index 290777f..5165c78 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -4,28 +4,80 @@ #include "ast.h" typedef enum FeTypeKind { - FE_TYPE_ERROR, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_INT, FE_TYPE_UNKNOWN + FE_TYPE_ERROR, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, + FE_TYPE_STRUCT, FE_TYPE_ENUM, FE_TYPE_ARRAY, FE_TYPE_SLICE, FE_TYPE_STR, + FE_TYPE_REF, FE_TYPE_UNKNOWN } FeTypeKind; +typedef struct FeFieldType FeFieldType; +typedef struct FeVariantType FeVariantType; + +struct FeFieldType { + char *name; + FeType *type; + unsigned long offset; + const FeNode *ast_node; +}; + +struct FeVariantType { + char *name; + FeFieldType *fields; + unsigned field_count; + unsigned tag; + const FeNode *ast_node; + char *maker; +}; + struct FeType { FeTypeKind kind; - char name[16]; + char name[64]; + char *cname; + char *maker; + char *indexer; + char *slicer; + char *full_slicer; + char *tail_slicer; unsigned bits; int is_unsigned; + int packed; + unsigned long length; + unsigned long size; + unsigned align; + FeType *elem; + int ref_mut; + FeFieldType *fields; + unsigned field_count; + FeVariantType *variants; + unsigned variant_count; FeType *next; + int emit_state; + int cycle_state; }; typedef struct FeTypeCtx { FeArena *arena; FeType *types; unsigned pointer_bits; + const char *unit_name; + unsigned generated_serial; } FeTypeCtx; void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits); FeType *fe_type_intern(FeTypeCtx *ctx, const char *name); FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node); +FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem); +FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem); +FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable); +FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed); +FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node); +void fe_type_layout_all(FeTypeCtx *ctx); +FeFieldType *fe_type_field(FeType *t, const char *name); +FeVariantType *fe_type_variant(FeType *t, const char *name); int fe_type_equal(const FeType *a, const FeType *b); int fe_type_is_integer(const FeType *t); +int fe_type_is_indexable(const FeType *t); const char *fe_type_c_name(const FeType *t, unsigned pointer_bits); +unsigned long fe_type_size(const FeType *t); +unsigned fe_type_align(const FeType *t); #endif diff --git a/fec/test-dos.bat b/fec/test-dos.bat index 5427a5e..22ce1f9 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -93,6 +93,102 @@ if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M2\BAD-VO.FE -o TESTS\M2\BAD-VO.C > nul if not errorlevel 1 goto test_fail +if exist TESTS\M3\STRUCT.C del TESTS\M3\STRUCT.C +if exist TESTS\M3\STRUCT.EXE del TESTS\M3\STRUCT.EXE +if exist TESTS\M3\ENUM.C del TESTS\M3\ENUM.C +if exist TESTS\M3\ENUM.EXE del TESTS\M3\ENUM.EXE +if exist TESTS\M3\ARRAY.C del TESTS\M3\ARRAY.C +if exist TESTS\M3\ARRAY.EXE del TESTS\M3\ARRAY.EXE +if exist TESTS\M3\STR.C del TESTS\M3\STR.C +if exist TESTS\M3\STR.EXE del TESTS\M3\STR.EXE +if exist TESTS\M3\FOR.C del TESTS\M3\FOR.C +if exist TESTS\M3\FOR.EXE del TESTS\M3\FOR.EXE +if exist TESTS\M3\NESTED.C del TESTS\M3\NESTED.C +if exist TESTS\M3\NESTED.EXE del TESTS\M3\NESTED.EXE +if exist TESTS\M3\CHAR.C del TESTS\M3\CHAR.C +if exist TESTS\M3\CHAR.EXE del TESTS\M3\CHAR.EXE +if exist TESTS\M3\ARRAYCTX.C del TESTS\M3\ARRAYCTX.C +if exist TESTS\M3\ARRAYCTX.EXE del TESTS\M3\ARRAYCTX.EXE +if exist TESTS\M3\BOUNDS.C del TESTS\M3\BOUNDS.C +if exist TESTS\M3\BOUNDS.EXE del TESTS\M3\BOUNDS.EXE +if exist TESTS\M3\BOUNDS-N.C del TESTS\M3\BOUNDS-N.C +if exist TESTS\M3\BOUNDS-N.EXE del TESTS\M3\BOUNDS-N.EXE + +fec.exe --target=bits32 --emit-c TESTS\M3\STRUCT.FE -o TESTS\M3\STRUCT.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\STRUCT.EXE TESTS\M3\STRUCT.C +if errorlevel 1 goto test_fail +TESTS\M3\STRUCT.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\ENUM.FE -o TESTS\M3\ENUM.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\ENUM.EXE TESTS\M3\ENUM.C +if errorlevel 1 goto test_fail +TESTS\M3\ENUM.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\ARRAY.FE -o TESTS\M3\ARRAY.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\ARRAY.EXE TESTS\M3\ARRAY.C +if errorlevel 1 goto test_fail +TESTS\M3\ARRAY.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\STR.FE -o TESTS\M3\STR.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\STR.EXE TESTS\M3\STR.C +if errorlevel 1 goto test_fail +TESTS\M3\STR.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\FOR.FE -o TESTS\M3\FOR.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\FOR.EXE TESTS\M3\FOR.C +if errorlevel 1 goto test_fail +TESTS\M3\FOR.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\NESTED.FE -o TESTS\M3\NESTED.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\NESTED.EXE TESTS\M3\NESTED.C +if errorlevel 1 goto test_fail +TESTS\M3\NESTED.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\CHAR.FE -o TESTS\M3\CHAR.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\CHAR.EXE TESTS\M3\CHAR.C +if errorlevel 1 goto test_fail +TESTS\M3\CHAR.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\ARRAYCTX.FE -o TESTS\M3\ARRAYCTX.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\ARRAYCTX.EXE TESTS\M3\ARRAYCTX.C +if errorlevel 1 goto test_fail +TESTS\M3\ARRAYCTX.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BOUNDS.FE -o TESTS\M3\BOUNDS.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\BOUNDS.EXE TESTS\M3\BOUNDS.C +if errorlevel 1 goto test_fail +TESTS\M3\BOUNDS.EXE +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --no-checks --emit-c TESTS\M3\BOUNDS.FE -o TESTS\M3\BOUNDS-N.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\BOUNDS-N.EXE TESTS\M3\BOUNDS-N.C +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADFLD.FE -o TESTS\M3\BADFLD.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADMAT.FE -o TESTS\M3\BADMAT.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADARR.FE -o TESTS\M3\BADARR.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADCYCLE.FE -o TESTS\M3\BADCYCLE.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADSTR.FE -o TESTS\M3\BADSTR.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADCHAR.FE -o TESTS\M3\BADCHAR.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADFIELD.FE -o TESTS\M3\BADFIELD.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BADINDEX.FE -o TESTS\M3\BADINDEX.C > nul +if not errorlevel 1 goto test_fail + echo OK>TEST.OK cd C:\FEC goto test_done diff --git a/fec/tests/m3/array.fe b/fec/tests/m3/array.fe new file mode 100644 index 0000000..a8ff358 --- /dev/null +++ b/fec/tests/m3/array.fe @@ -0,0 +1,9 @@ +unit m3_array; + +fn main() -> i32 { + let a: [3]i32 = [1, 2, 3]; + let s: []i32 = a[..]; + let t: []i32 = s[1..3]; + if a[0] + s[1] + t[0] == 5 and s.n == 3 { return 0; } + return 1; +} diff --git a/fec/tests/m3/arrayctx.fe b/fec/tests/m3/arrayctx.fe new file mode 100644 index 0000000..9e6d047 --- /dev/null +++ b/fec/tests/m3/arrayctx.fe @@ -0,0 +1,7 @@ +unit m3_arrayctx; + +fn main() -> i32 { + let bytes: [3]u8 = [1, 2, 3]; + if bytes[0] == 1 and bytes[2] == 3 { return 0; } + return 1; +} diff --git a/fec/tests/m3/badarr.fe b/fec/tests/m3/badarr.fe new file mode 100644 index 0000000..f419f7a --- /dev/null +++ b/fec/tests/m3/badarr.fe @@ -0,0 +1,5 @@ +unit fail_m3_array; +fn main() -> i32 { + let a: [2]i32 = [1, true, 3]; + return a[0]; +} diff --git a/fec/tests/m3/badchar.fe b/fec/tests/m3/badchar.fe new file mode 100644 index 0000000..aed42d4 --- /dev/null +++ b/fec/tests/m3/badchar.fe @@ -0,0 +1,6 @@ +unit fail_m3_char; + +fn main() -> i32 { + let u: u8 = 'A'; + return u; +} diff --git a/fec/tests/m3/badcycle.fe b/fec/tests/m3/badcycle.fe new file mode 100644 index 0000000..e96c17e --- /dev/null +++ b/fec/tests/m3/badcycle.fe @@ -0,0 +1,6 @@ +unit fail_m3_cycle; + +struct A { b: B, } +struct B { a: A, } + +fn main() -> i32 { return 0; } diff --git a/fec/tests/m3/badfield.fe b/fec/tests/m3/badfield.fe new file mode 100644 index 0000000..350edbb --- /dev/null +++ b/fec/tests/m3/badfield.fe @@ -0,0 +1,8 @@ +unit fail_m3_let_field; + +struct Point { x: i32, y: i32, } +fn main() -> i32 { + let p: Point = Point{ x: 1, y: 2 }; + p.x = 3; + return p.x; +} diff --git a/fec/tests/m3/badfld.fe b/fec/tests/m3/badfld.fe new file mode 100644 index 0000000..dd9e0c4 --- /dev/null +++ b/fec/tests/m3/badfld.fe @@ -0,0 +1,6 @@ +unit fail_m3_fields; +struct Point { x: i32, y: i32, } +fn main() -> i32 { + let p: Point = Point{ x: 1 }; + return p.z; +} diff --git a/fec/tests/m3/badindex.fe b/fec/tests/m3/badindex.fe new file mode 100644 index 0000000..88e8bca --- /dev/null +++ b/fec/tests/m3/badindex.fe @@ -0,0 +1,7 @@ +unit fail_m3_let_index; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + a[0] = 3; + return a[0]; +} diff --git a/fec/tests/m3/badmat.fe b/fec/tests/m3/badmat.fe new file mode 100644 index 0000000..f1c1d54 --- /dev/null +++ b/fec/tests/m3/badmat.fe @@ -0,0 +1,6 @@ +unit fail_m3_match; +enum Shape { Empty, Circle(i32), } +fn main() -> i32 { + match Shape.Empty { Empty => 0; } + return 0; +} diff --git a/fec/tests/m3/badstr.fe b/fec/tests/m3/badstr.fe new file mode 100644 index 0000000..2896f0c --- /dev/null +++ b/fec/tests/m3/badstr.fe @@ -0,0 +1,6 @@ +unit fail_m3_str; +fn main() -> i32 { + var text: str = "abc"; + text[0] = 'z'; + return 0; +} diff --git a/fec/tests/m3/bounds.fe b/fec/tests/m3/bounds.fe new file mode 100644 index 0000000..ac581ee --- /dev/null +++ b/fec/tests/m3/bounds.fe @@ -0,0 +1,6 @@ +unit m3_bounds; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + return a[2]; +} diff --git a/fec/tests/m3/char.fe b/fec/tests/m3/char.fe new file mode 100644 index 0000000..253ff85 --- /dev/null +++ b/fec/tests/m3/char.fe @@ -0,0 +1,9 @@ +unit m3_char; + +fn main() -> i32 { + let c: char = '\u0041'; + let u: u8 = c as u8; + let d: char = u as char; + if c == d and u == ('A' as u8) { return 0; } + return 1; +} diff --git a/fec/tests/m3/enum.fe b/fec/tests/m3/enum.fe new file mode 100644 index 0000000..2e2e934 --- /dev/null +++ b/fec/tests/m3/enum.fe @@ -0,0 +1,16 @@ +unit m3_enum; + +enum Shape { Empty, Circle(i32), Rect { w: i32, h: i32, }, } + +fn score(s: Shape) -> i32 { + match s { + Empty => { return 0; } + Circle(value) => { return value; } + Rect { w, h } => { return w * h; } + } +} + +fn main() -> i32 { + if score(Shape.Circle(5)) == 5 and score(Shape.Rect{ w: 2, h: 3 }) == 6 { return 0; } + return 1; +} diff --git a/fec/tests/m3/for.fe b/fec/tests/m3/for.fe new file mode 100644 index 0000000..e591000 --- /dev/null +++ b/fec/tests/m3/for.fe @@ -0,0 +1,18 @@ +unit m3_for; + +fn main() -> i32 { + var total: i32 = 0; + let a: [3]i32 = [1, 2, 3]; + let s: []i32 = a[..]; + var m: [1]i32 = [1]; + let ready: bool = true; + if ready { total += 0; } + while false { total += 100; } + for x in a { total += x.^; } + for i, x in a { if i == 1 and x.^ == 2 { total += 1; } } + for x in "ab" { if x.^ == ('a' as u8) { total += 1; } } + for x in s { if x.^ == 3 { total += 1; } } + for x in m { x.^ = 2; } + if total == 9 and m[0] == 2 { return 0; } + return 1; +} diff --git a/fec/tests/m3/nested.fe b/fec/tests/m3/nested.fe new file mode 100644 index 0000000..3f71cc9 --- /dev/null +++ b/fec/tests/m3/nested.fe @@ -0,0 +1,10 @@ +unit m3_nested; + +struct Outer { inner: Inner, } +struct Inner { value: i32, } + +fn main() -> i32 { + let x: Outer = Outer{ inner: Inner{ value: 7 } }; + if x.inner.value == 7 { return 0; } + return 1; +} diff --git a/fec/tests/m3/str.fe b/fec/tests/m3/str.fe new file mode 100644 index 0000000..f0077fd --- /dev/null +++ b/fec/tests/m3/str.fe @@ -0,0 +1,7 @@ +unit m3_str; + +fn main() -> i32 { + let text: str = "abc"; + if text[1] == ('b' as u8) and text.n == 3 { return 0; } + return 1; +} diff --git a/fec/tests/m3/struct.fe b/fec/tests/m3/struct.fe new file mode 100644 index 0000000..2d7e2b1 --- /dev/null +++ b/fec/tests/m3/struct.fe @@ -0,0 +1,14 @@ +unit m3_struct; + +struct Point { x: i32, y: i32, } +packed struct PackedPoint { x: u8, y: i32, } +struct Natural { a: u8, b: i32, } + +fn main() -> i32 { + let p: Point = Point{ x: 3, y: 4 }; + let q: PackedPoint = PackedPoint{ x: 1, y: 6 }; + let n: Natural = Natural{ a: 1, b: 2 }; + if p.x + p.y == 7 and q.y == 6 and n.b == 2 and + (Point{ x: 1, y: 2 }.x == 1) and @size_of(Natural) == 8 { return 0; } + return 1; +} diff --git a/fec/tests/run-tests.sh b/fec/tests/run-tests.sh index d7b4d75..9b907cc 100644 --- a/fec/tests/run-tests.sh +++ b/fec/tests/run-tests.sh @@ -40,3 +40,24 @@ for f in bad-condition bad-cast bad-assign bad-unknown bad-arity bad-types bad-r fi done echo "M2 tests: integer control-flow smoke passed" + +m3tmp=$(mktemp -d) +trap 'rm -rf "$m2tmp" "$m3tmp"' EXIT HUP INT TERM +for f in struct enum array arrayctx str for nested char; do + "$root"/fec --target=bits32 --emit-c "$root"/tests/m3/$f.fe -o "$m3tmp/$f.c" + ${CC:-cc} -std=c89 -pedantic "$m3tmp/$f.c" -o "$m3tmp/$f" + "$m3tmp/$f" +done +"$root"/fec --target=bits32 --emit-c "$root"/tests/m3/bounds.fe -o "$m3tmp/bounds.c" +${CC:-cc} -std=c89 -pedantic "$m3tmp/bounds.c" -o "$m3tmp/bounds" +if "$m3tmp/bounds"; then + echo "FAIL (bounds trap did not fire): tests/m3/bounds.fe" + exit 1 +fi +for f in badfld badmat badarr badcycle badstr badchar badfield badindex; do + if "$root"/fec --target=bits32 --emit-c "$root"/tests/m3/$f.fe -o "$m3tmp/$f.c" >/dev/null 2>/dev/null; then + echo "FAIL (accepted M3 semantic error): $f.fe" + exit 1 + fi +done +echo "M3 tests: structs, enums, arrays, slices, str, match, and bounds passed" diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat index af2b722..63f9cb7 100644 --- a/fec/vm-m1.bat +++ b/fec/vm-m1.bat @@ -7,6 +7,7 @@ if not exist C:\FEC\TESTS md C:\FEC\TESTS if not exist C:\FEC\TESTS\PASS md C:\FEC\TESTS\PASS if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL if not exist C:\FEC\TESTS\M2 md C:\FEC\TESTS\M2 +if not exist C:\FEC\TESTS\M3 md C:\FEC\TESTS\M3 if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL if exist C:\FEC\STAGE.FAIL del C:\FEC\STAGE.FAIL @@ -106,6 +107,41 @@ if errorlevel 1 goto stage_fail copy D:\FEC\TESTS\M2\CAST-W~1.FE C:\FEC\TESTS\M2\CAST-W.FE > nul if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\STRUCT.FE C:\FEC\TESTS\M3\STRUCT.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\ENUM.FE C:\FEC\TESTS\M3\ENUM.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\ARRAY.FE C:\FEC\TESTS\M3\ARRAY.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\STR.FE C:\FEC\TESTS\M3\STR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\FOR.FE C:\FEC\TESTS\M3\FOR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\NESTED.FE C:\FEC\TESTS\M3\NESTED.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\CHAR.FE C:\FEC\TESTS\M3\CHAR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\ARRAYCTX.FE C:\FEC\TESTS\M3\ARRAYCTX.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BOUNDS.FE C:\FEC\TESTS\M3\BOUNDS.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADFLD.FE C:\FEC\TESTS\M3\BADFLD.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADMAT.FE C:\FEC\TESTS\M3\BADMAT.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADARR.FE C:\FEC\TESTS\M3\BADARR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADCYCLE.FE C:\FEC\TESTS\M3\BADCYCLE.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADSTR.FE C:\FEC\TESTS\M3\BADSTR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADCHAR.FE C:\FEC\TESTS\M3\BADCHAR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADFIELD.FE C:\FEC\TESTS\M3\BADFIELD.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M3\BADINDEX.FE C:\FEC\TESTS\M3\BADINDEX.FE > nul +if errorlevel 1 goto stage_fail + call C:\FEC\TEST-DOS.BAT if exist C:\FEC\TEST.OK goto vm_success echo FAIL>C:\FEC\VM.FAIL From 53bca214b82c3abf7882ad41fb177cc488e999af Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 13:24:25 +0900 Subject: [PATCH 010/184] feat: implement M4 formatting builtins --- fec/src/check.c | 188 ++++++++++++++++++++- fec/src/emit_c.c | 316 ++++++++++++++++++++++++++++++++++-- fec/src/emit_c.h | 1 + fec/src/parser.c | 13 +- fec/src/types.c | 53 +++++- fec/src/types.h | 7 +- fec/test-dos.bat | 39 +++++ fec/tests/m4/bad-arity.fe | 6 + fec/tests/m4/bad-cls.fe | 6 + fec/tests/m4/bad-many.fe | 6 + fec/tests/m4/bad-open.fe | 6 + fec/tests/m4/bad-runtime.fe | 7 + fec/tests/m4/bad-try.fe | 6 + fec/tests/m4/bad-type.fe | 9 + fec/tests/m4/bad-verb.fe | 6 + fec/tests/m4/bad-writer.fe | 7 + fec/tests/m4/format.fe | 30 ++++ fec/tests/m4/prop.fe | 5 + fec/tests/m4/proptest.c | 23 +++ fec/tests/m4/try-fprint.fe | 8 + fec/tests/run-tests.sh | 30 ++++ fec/vm-m1.bat | 28 ++++ 22 files changed, 779 insertions(+), 21 deletions(-) create mode 100644 fec/tests/m4/bad-arity.fe create mode 100644 fec/tests/m4/bad-cls.fe create mode 100644 fec/tests/m4/bad-many.fe create mode 100644 fec/tests/m4/bad-open.fe create mode 100644 fec/tests/m4/bad-runtime.fe create mode 100644 fec/tests/m4/bad-try.fe create mode 100644 fec/tests/m4/bad-type.fe create mode 100644 fec/tests/m4/bad-verb.fe create mode 100644 fec/tests/m4/bad-writer.fe create mode 100644 fec/tests/m4/format.fe create mode 100644 fec/tests/m4/prop.fe create mode 100644 fec/tests/m4/proptest.c create mode 100644 fec/tests/m4/try-fprint.fe diff --git a/fec/src/check.c b/fec/src/check.c index 7714945..9563b18 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -12,6 +12,7 @@ struct FeSym { FeNode *fn; int mutable; int initialized; + FeNode *decl; }; struct FeScope { @@ -192,6 +193,7 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, sym->fn = fn; sym->mutable = mutable; sym->initialized = initialized; + sym->decl = decl; if (decl) { decl->cname = cname; decl->sem_type = type; @@ -215,6 +217,128 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n); static void check_match(FeCheckerState *s, FeNode *n); static void check_stmt(FeCheckerState *s, FeNode *n); +static FeNode *find_const_node(FeCheck *c, const char *name) +{ + FeNode *n; + for (n=c->ast->root ? c->ast->root->children : 0; n; n=n->next) + if (n->kind==FE_N_CONST && n->text && name && strcmp(n->text,name)==0) + return n; + return 0; +} + +static const char *builtin_format(FeCheckerState *s, FeNode *fmt) +{ + FeNode *decl; + FeSym *sym; + if (fmt && fmt->kind==FE_N_LITERAL && fmt->text && fmt->text[0]=='"') + return fmt->text; + if (fmt && fmt->kind==FE_N_IDENT) { + sym=find_symbol(s->scope,fmt->text); + decl=sym && sym->decl && sym->decl->kind==FE_N_CONST ? + sym->decl : find_const_node(s->c,fmt->text); + if (decl && decl->b && decl->b->kind==FE_N_LITERAL && + decl->b->text && decl->b->text[0]=='"') { + if (!decl->a || fe_type_from_ast(&s->c->types,decl->a)->kind==FE_TYPE_STR) + return decl->b->text; + } + } + return 0; +} + +static int format_is_slice_u8(FeType *t) +{ + return t && t->kind==FE_TYPE_SLICE && t->elem && + t->elem->kind==FE_TYPE_INT && strcmp(t->elem->name,"u8")==0; +} + +static int format_is_writer_type(FeType *t) +{ + return t && t->kind==FE_TYPE_STRUCT && + (strcmp(t->name,"Writer")==0 || strcmp(t->name,"io.Writer")==0); +} + +static int format_arg_ok(FeType *t, int verb) +{ + if (!t) return 0; + if (verb=='x') return fe_type_is_integer(t); + if (verb=='c') return t->kind==FE_TYPE_CHAR; + if (verb=='s') return t->kind==FE_TYPE_STR || format_is_slice_u8(t); + if (verb=='b') return t->kind==FE_TYPE_BOOL; + if (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_BOOL || + t->kind==FE_TYPE_CHAR || t->kind==FE_TYPE_STR) return 1; + return format_is_slice_u8(t) || + (t->kind==FE_TYPE_ENUM && t->is_error); +} + +static void check_format_call(FeCheckerState *s, FeNode *n) +{ + const char *fmt; + FeNode *fmt_node; + FeNode *arg; + FeNode *x; + FeType *t; + unsigned long i,j; + unsigned count=0; + unsigned argc=0; + unsigned offset=0; + int verb; + int bad=0; + if (strcmp(n->text,"@fprint")==0) offset=1; + fmt_node=n->children; + if (offset) { + if (!fmt_node) { err(s->c,n->loc,"@fprint requires a writer"); return; } + t=check_expr(s,fmt_node); + if (!(t && t->kind==FE_TYPE_REF && t->ref_mut && + format_is_writer_type(t->elem))) + err(s->c,fmt_node->loc,"@fprint requires &mut io.Writer"); + fmt_node=fmt_node->next; + } + if (strcmp(n->text,"@sprint")==0) { + if (!fmt_node) { err(s->c,n->loc,"@sprint requires a buffer"); return; } + t=check_expr(s,fmt_node); + if (!format_is_slice_u8(t)) err(s->c,fmt_node->loc,"@sprint requires []u8 buffer"); + fmt_node=fmt_node->next; + } + fmt=builtin_format(s,fmt_node); + if (!fmt) { err(s->c,n->loc,"format must be a comptime string"); return; } + n->aux_text=(char *)fmt; + arg=fmt_node ? fmt_node->next : 0; + for (x=arg;x;x=x->next) { check_expr(s,x); ++argc; } + i=1; + while (fmt[i] && fmt[i]!='"') { + if (fmt[i]=='\\') { if (fmt[i+1]) ++i; ++i; continue; } + if (fmt[i]=='{' && fmt[i+1]=='{') { i+=2; continue; } + if (fmt[i]=='}' && fmt[i+1]=='}') { i+=2; continue; } + if (fmt[i]=='{') { + j=i+1; + while (fmt[j] && fmt[j]!='}') ++j; + if (!fmt[j]) { err(s->c,n->loc,"unterminated format placeholder"); bad=1; break; } + if (j==i+1) verb=' '; else if (j==i+2) verb=(unsigned char)fmt[i+1]; else verb='?'; + if (verb!=' ' && verb!='x' && verb!='c' && verb!='s' && verb!='b') { + err(s->c,n->loc,"unsupported format verb"); bad=1; + } + if (!arg) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } + else { + t=arg->sem_type; + if (verb==' ' && t && t->kind==FE_TYPE_ENUM && t->is_error) verb='s'; + if (!format_arg_ok(t,verb)) { err(s->c,arg->loc,"no fmt writer for argument type"); bad=1; } + arg=arg->next; + } + ++count; i=j+1; continue; + } + if (fmt[i]=='}') { err(s->c,n->loc,"unmatched '}' in format"); bad=1; } + ++i; + } + if (count!=argc) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } + (void)bad; +} + +static int is_format_builtin(const char *name) +{ + return name && (strcmp(name,"@print")==0 || strcmp(name,"@fprint")==0 || + strcmp(name,"@sprint")==0); +} + static int lvalue_writable(FeCheckerState *s, FeNode *n) { FeSym *sym; @@ -292,12 +416,21 @@ static FeType *check_array_init(FeCheckerState *s, FeNode *n) n->sem_type=fe_type_array(&s->c->types,count,elem); return n->sem_type; } +static int array_slice_lvalue(FeNode *n) +{ + return n && (n->kind==FE_N_IDENT || n->kind==FE_N_MEMBER || + n->kind==FE_N_INDEX); +} + static FeType *check_index(FeCheckerState *s, FeNode *n) { FeType *base=check_expr(s,n->a); FeType *idx; FeType *elem; if(!fe_type_is_indexable(base)) { err(s->c,n->loc,"indexing requires an array or slice"); return unknown(s->c); } if(n->b) { idx=check_expr(s,n->b); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"index must be an integer"); } - if(n->c || !n->b) { if(n->c) { idx=check_expr(s,n->c); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"slice bound must be an integer"); } elem=base->elem; n->sem_type=fe_type_slice(&s->c->types,elem); if(base->kind==FE_TYPE_STR) n->flags|=2U; return n->sem_type; } + if(n->c || !n->b) { + if (base->kind==FE_TYPE_ARRAY && !array_slice_lvalue(n->a)) + err(s->c,n->loc,"array slicing requires a stable lvalue"); + if(n->c) { idx=check_expr(s,n->c); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"slice bound must be an integer"); } elem=base->elem; n->sem_type=fe_type_slice(&s->c->types,elem); if(base->kind==FE_TYPE_STR) n->flags|=2U; return n->sem_type; } n->sem_type=base->elem; return n->sem_type; } @@ -361,6 +494,15 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) } else if (strcmp(op, "-") == 0) { if (known(a) && !fe_type_is_integer(a)) err(c, n->loc, "unary '-' requires integer"); + } else if (strcmp(op, "try") == 0) { + if (a && a->kind==FE_TYPE_ERROR_UNION) + a=a->error_value; + else { + err(c,n->loc,"try requires an error result"); + a=unknown(c); + } + } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + a=fe_type_ref(&c->types,a,strcmp(op,"&mut")==0); } n->sem_type = a; return a; @@ -403,6 +545,34 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return a; } if (n->kind == FE_N_CALL) { + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && + (strcmp(n->a->b->text,"buf_writer")==0 || + strcmp(n->a->b->text,"null_writer")==0)) { + FeNode *arg=n->children; + if (strcmp(n->a->b->text,"buf_writer")==0) { + if (!arg) err(c,n->loc,"io.buf_writer requires a buffer"); + else { + a=check_expr(s,arg); + if (!(a && a->kind==FE_TYPE_REF && a->ref_mut && + format_is_slice_u8(a->elem))) + err(c,arg->loc,"io.buf_writer requires &mut []u8 buffer"); + } + } else if (arg) err(c,n->loc,"io.null_writer takes no arguments"); + n->sem_type=fe_type_intern(&c->types,"io.Writer"); + return n->sem_type; + } + if (n->text && is_format_builtin(n->text)) { + check_format_call(s,n); + if (strcmp(n->text,"@print")==0) + n->sem_type=fe_type_intern(&c->types,"void"); + else if (strcmp(n->text,"@sprint")==0) + n->sem_type=fe_type_intern(&c->types,"usize"); + else + n->sem_type=fe_type_error_union(&c->types,fe_type_intern(&c->types,"void")); + return n->sem_type; + } if (!n->a && n->text && (strcmp(n->text,"@size_of")==0 || strcmp(n->text,"@align_of")==0)) { FeNode *type_arg=n->children; FeType *target=type_arg && type_arg->kind==FE_N_IDENT ? fe_type_intern(&c->types,type_arg->text) : unknown(c); @@ -452,6 +622,12 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return unknown(c); } if (n->kind == FE_N_MEMBER) { + if (n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"io")==0 && n->b && n->b->text && + strcmp(n->b->text,"stdout")==0) { + n->sem_type=fe_type_intern(&c->types,"io.Writer"); + return n->sem_type; + } a=check_expr(s,n->a); if (a->kind == FE_TYPE_REF && n->b && n->b->text && strcmp(n->b->text,"^")==0) { @@ -645,6 +821,10 @@ static void check_type_cycle(FeCheck *c, FeType *t) t->kind == FE_TYPE_INT || t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR || t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) return; + if (t->kind == FE_TYPE_ERROR_UNION) { + check_type_cycle(c,t->error_value); + return; + } if (t->cycle_state == 1) { if (c->ast->root) err(c, c->ast->root->loc, "by-value recursive type"); return; @@ -739,6 +919,10 @@ static void check_stmt(FeCheckerState *s, FeNode *n) break; case FE_N_EXPR_STMT: check_expr(s, n->a); + if (n->a && n->a->kind==FE_N_UNARY && n->a->text && + strcmp(n->a->text,"try")==0 && + (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION)) + err(c,n->loc,"try requires an enclosing error result"); break; case FE_N_IF: a = check_expr(s, n->a); @@ -815,6 +999,8 @@ int fe_check_program(FeCheck *c) fe_type_declare_struct(&c->types, n, (n->flags & 1U) != 0); for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) if (n->kind == FE_N_ENUM) fe_type_declare_enum(&c->types, n); + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) + if (n->kind == FE_N_ERROR_DECL) fe_type_declare_error(&c->types, n); check_type_cycles(c); fe_type_layout_all(&c->types); for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 04dd5f5..ff75623 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -40,6 +40,7 @@ static void emit_type_deps(FeEmitter *e, FeType *t) static void emit_one_type(FeEmitter *e, FeType *t) { unsigned i,j; + if (t && strcmp(t->name,"io.Writer")==0) return; if (!t || t->emit_state || (t->kind != FE_TYPE_STRUCT && t->kind != FE_TYPE_ENUM && t->kind != FE_TYPE_ARRAY && t->kind != FE_TYPE_SLICE)) return; @@ -114,12 +115,12 @@ static void emit_type_helpers(FeEmitter *e) fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); if(!e->no_checks) fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); fprintf(e->out,"return x.a[i]; }\n"); - fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", + fprintf(e->out,"static %s %s(%s *x, unsigned long a, unsigned long b) { ", fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->slicer,t->cname); if(!e->no_checks) fputs("if (a > b || b > ",e->out), fprintf(e->out,"%lu",t->length), fputs(") fe_trap_bounds(); ",e->out); - fprintf(e->out,"return %s(x.a+a,b-a); }\n",fe_type_slice(&e->check->types,t->elem)->maker); - fprintf(e->out,"static %s %s(%s x) { return %s(x,0,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->full_slicer,t->cname,t->slicer,t->length); - fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->tail_slicer,t->cname,t->slicer,t->length); + fprintf(e->out,"return %s(x->a+a,b-a); }\n",fe_type_slice(&e->check->types,t->elem)->maker); + fprintf(e->out,"static %s %s(%s *x) { return %s(x,0,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->full_slicer,t->cname,t->slicer,t->length); + fprintf(e->out,"static %s %s(%s *x, unsigned long a) { return %s(x,a,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->tail_slicer,t->cname,t->slicer,t->length); } else if (t->kind==FE_TYPE_SLICE && t->indexer) { fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); @@ -143,6 +144,49 @@ static void emit_type_helpers(FeEmitter *e) fputs("static fe_str fe_tail_slice_str(fe_str x, unsigned long a) { return fe_slice_str(x,a,x.n); }\n",e->out); } +static void emit_m4_runtime(FeEmitter *e) +{ + fputs("typedef struct { unsigned char *p; unsigned long n; } fe_m4_slice;\n",e->out); + fputs("typedef struct { void *ctx; unsigned short (*write_fn)(void *, const unsigned char *, unsigned long); } fe_writer;\n",e->out); + fputs("unsigned short fe_m4_error;\n",e->out); + fputs("unsigned short fe_m4_stdout_write(void *ctx, const unsigned char *p, unsigned long n) { (void)ctx; return fwrite(p,1,(size_t)n,stdout)==(size_t)n ? 0 : 1; }\n",e->out); + fputs("unsigned short fe_m4_null_write(void *ctx, const unsigned char *p, unsigned long n) { (void)ctx; (void)p; (void)n; return 0; }\n",e->out); + fputs("unsigned short fe_m4_buf_write(void *ctx, const unsigned char *p, unsigned long n) { fe_m4_slice *b=(fe_m4_slice*)ctx; unsigned long k=nn?n:b->n; if(k) memcpy(b->p,p,(size_t)k); b->p+=k; b->n-=k; return 0; }\n",e->out); + fputs("fe_writer fe_m4_stdout_writer(void) { fe_writer w; w.ctx=0; w.write_fn=fe_m4_stdout_write; return w; }\n",e->out); + fputs("fe_writer fe_m4_null_writer(void) { fe_writer w; w.ctx=0; w.write_fn=fe_m4_null_write; return w; }\n",e->out); + fputs("fe_writer fe_m4_buf_writer(fe_m4_slice *b) { fe_writer w; w.ctx=b; w.write_fn=fe_m4_buf_write; return w; }\n",e->out); + fputs("/* bounded sprint stack; overflow traps instead of corrupting an outer call */\n#define FE_M4_SPRINT_DEPTH 8\n",e->out); + fputs("typedef struct { fe_m4_slice b; unsigned long start_n; } fe_m4_sprint_frame;\n",e->out); + fputs("static fe_m4_sprint_frame fe_m4_sprint_stack[FE_M4_SPRINT_DEPTH];\n",e->out); + fputs("static unsigned fe_m4_sprint_depth;\n",e->out); + fputs("void fe_m4_sprint_begin(fe_m4_slice *b) { if (fe_m4_sprint_depth>=FE_M4_SPRINT_DEPTH) abort(); fe_m4_sprint_stack[fe_m4_sprint_depth].b=*b; fe_m4_sprint_stack[fe_m4_sprint_depth].start_n=b->n; ++fe_m4_sprint_depth; }\n",e->out); + fputs("fe_writer fe_m4_sprint_writer(void) { return fe_m4_buf_writer(&fe_m4_sprint_stack[fe_m4_sprint_depth-1].b); }\n",e->out); + fputs("unsigned long fe_m4_sprint_finish(void) { unsigned long result; if (!fe_m4_sprint_depth) abort(); --fe_m4_sprint_depth; result=fe_m4_sprint_stack[fe_m4_sprint_depth].start_n-fe_m4_sprint_stack[fe_m4_sprint_depth].b.n; return result; }\n",e->out); + fputs("unsigned short fe_m4_write_bytes(fe_writer w, const unsigned char *p, unsigned long n) { return w.write_fn ? w.write_fn(w.ctx,p,n) : 1; }\n",e->out); + fputs("unsigned short fe_m4_write_cstr(fe_writer w, const char *p) { return fe_m4_write_bytes(w,(const unsigned char*)p,(unsigned long)strlen(p)); }\n",e->out); + fputs("unsigned short fe_m4_write_str(fe_writer w, fe_str s) { return fe_m4_write_bytes(w,s.p,s.n); }\n",e->out); + fputs("#define fe_m4_write_slice(w,s) fe_m4_write_bytes((w),(s).p,(s).n)\n",e->out); + fputs("unsigned short fe_m4_write_int(fe_writer w, long v) { char b[40]; sprintf(b,\"%ld\",v); return fe_m4_write_cstr(w,b); }\n",e->out); + fputs("unsigned short fe_m4_write_hex(fe_writer w, unsigned long v) { char b[40]; sprintf(b,\"%lx\",v); return fe_m4_write_cstr(w,b); }\n",e->out); + fputs("unsigned short fe_m4_write_char(fe_writer w, unsigned char v) { return fe_m4_write_bytes(w,&v,1); }\n",e->out); + fputs("unsigned short fe_m4_write_bool(fe_writer w, unsigned char v) { return fe_m4_write_cstr(w,v ? \"true\" : \"false\"); }\n",e->out); + fputs("unsigned short fe_m4_write_error(fe_writer w, unsigned long v) { char b[40]; sprintf(b,\"error#%lu\",v); return fe_m4_write_cstr(w,b); }\n",e->out); +} + +static int node_uses_m4(FeNode *n) +{ + FeNode *x; + if (!n) return 0; + if (n->kind==FE_N_CALL && n->text && + (strcmp(n->text,"@print")==0 || strcmp(n->text,"@fprint")==0 || + strcmp(n->text,"@sprint")==0)) return 1; + if (n->kind==FE_N_CALL && n->a && n->a->kind==FE_N_MEMBER && + n->a->a && n->a->a->text && strcmp(n->a->a->text,"io")==0) return 1; + if (node_uses_m4(n->a) || node_uses_m4(n->b) || node_uses_m4(n->c)) return 1; + for (x=n->children; x; x=x->next) if (node_uses_m4(x)) return 1; + return 0; +} + static void emit_expr(FeEmitter *e, FeNode *n); static void emit_stmt(FeEmitter *e, FeNode *n); @@ -267,6 +311,187 @@ static void emit_c_literal(FILE *out, const char *text, int string) fputc(quote,out); } +static void emit_m4_piece(FILE *out, const char *fmt, unsigned long begin, + unsigned long end) +{ + unsigned long i; + unsigned long cp; + int h0,h1,h2,h3; + int c; + fputc('"',out); + for(i=begin;i=0 && + (h1=hex_value((unsigned char)fmt[i+2]))>=0) { + emit_byte(out,(unsigned)((h0<<4)|h1)); i+=2; + } else if(c=='u' && i+4=0 && + (h1=hex_value((unsigned char)fmt[i+2]))>=0 && + (h2=hex_value((unsigned char)fmt[i+3]))>=0 && + (h3=hex_value((unsigned char)fmt[i+4]))>=0) { + cp=(unsigned long)((h0<<12)|(h1<<8)|(h2<<4)|h3); + emit_codepoint(out,cp); i+=4; + } + else if(c=='\\' || c=='"') { fputc('\\',out); fputc(c,out); } + else emit_byte(out,(unsigned)c); + continue; + } + if(c=='"' || c=='\\') fputc('\\',out); + fputc(c,out); + } + fputc('"',out); +} + +static void emit_m4_writer(FeEmitter *e, FeNode *arg, int buffer) +{ + if (buffer) { + fputs("fe_m4_buf_writer((fe_m4_slice*)&",e->out); + if (arg && arg->kind==FE_N_UNARY && arg->text && + (strcmp(arg->text,"&")==0 || strcmp(arg->text,"&mut")==0)) emit_expr(e,arg->a); + else emit_expr(e,arg); + fputs(")",e->out); + } else if (arg && arg->kind==FE_N_UNARY && arg->text && + (strcmp(arg->text,"&")==0 || strcmp(arg->text,"&mut")==0)) { + emit_expr(e,arg->a); + } else if (arg && arg->kind==FE_N_CALL && arg->a && + arg->a->kind==FE_N_MEMBER) { + emit_expr(e,arg); + } else if (arg && arg->sem_type && arg->sem_type->kind==FE_TYPE_REF) { + fputs("(*",e->out); emit_expr(e,arg); fputc(')',e->out); + } else { + emit_expr(e,arg); + } +} + +static void emit_m4_writer_value(FeEmitter *e, FeNode *writer, int buffer) +{ + if (!writer) fputs("fe_m4_stdout_writer()",e->out); + else if (buffer) fputs("fe_m4_sprint_writer()",e->out); + else emit_m4_writer(e,writer,buffer); +} + +static void emit_m4_arg(FeEmitter *e, FeNode *arg, int verb, + FeNode *writer, int buffer, int error_value) +{ + FeType *t=arg ? arg->sem_type : 0; + if (verb=='x') { + fputs("fe_m4_write_hex(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned long)",e->out); emit_expr(e,arg); fputc(')',e->out); return; + } + if (verb=='c') { + fputs("fe_m4_write_char(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned char)",e->out); emit_expr(e,arg); fputc(')',e->out); return; + } + if (verb=='b') { + fputs("fe_m4_write_bool(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; + } + if (verb=='s' || (verb==' ' && t && (t->kind==FE_TYPE_STR || t->kind==FE_TYPE_SLICE))) { + if (t && t->kind==FE_TYPE_STR) fputs("fe_m4_write_str(",e->out); + else fputs("fe_m4_write_slice(",e->out); + emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; + } + if (error_value) { + fputs("fe_m4_write_error(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned long)",e->out); emit_expr(e,arg); fputs(".tag)",e->out); return; + } + if (verb==' ' && t && t->kind==FE_TYPE_BOOL) { + fputs("fe_m4_write_bool(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; + } + if (verb==' ' && t && t->kind==FE_TYPE_CHAR) { + fputs("fe_m4_write_char(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned char)",e->out); emit_expr(e,arg); fputc(')',e->out); return; + } + fputs("fe_m4_write_int(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (long)",e->out); emit_expr(e,arg); fputc(')',e->out); +} + +static void emit_m4_builtin(FeEmitter *e, FeNode *n) +{ + const char *fmt=n->aux_text; + FeNode *fmt_node=n->children; + FeNode *arg; + FeNode *writer_arg=0; + FeNode *buffer_arg=0; + unsigned long i,j,last=1; + unsigned count=0; + int verb; + int error_value; + int first=1; + int is_print=strcmp(n->text,"@print")==0; + int is_sprint=strcmp(n->text,"@sprint")==0; + if (!fmt) { fputs("0",e->out); return; } + if (is_print) writer_arg=0; + else if (is_sprint) { buffer_arg=fmt_node; fmt_node=fmt_node ? fmt_node->next : 0; } + else { writer_arg=fmt_node; fmt_node=fmt_node ? fmt_node->next : 0; } + arg=fmt_node ? fmt_node->next : 0; + error_value=0; + fputc('(',e->out); + if (!is_print && !is_sprint) { + fputs("fe_m4_error=0",e->out); + first=0; + } + if (is_sprint) { + fputs("fe_m4_sprint_begin((fe_m4_slice*)&",e->out); emit_expr(e,buffer_arg); fputs(")",e->out); + first=0; + } + i=1; + while(fmt[i] && fmt[i]!='"') { + if(fmt[i]=='\\') { ++i; if(fmt[i]) ++i; continue; } + if(fmt[i]=='{' && fmt[i+1]=='{') { i+=2; continue; } + if(fmt[i]=='}' && fmt[i+1]=='}') { i+=2; continue; } + if(fmt[i]=='{') { + j=i+1; while(fmt[j] && fmt[j]!='}') ++j; + if(!fmt[j]) break; + if(i>last) { + if(!first) fputs(", ",e->out); + if (!is_print && !is_sprint) + fputs("fe_m4_error ? fe_m4_error : (fe_m4_error = ",e->out); + if(is_print) { fputs("fe_m4_write_cstr(fe_m4_stdout_writer(), ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } + else { fputs("fe_m4_write_cstr(",e->out); if(is_sprint) emit_m4_writer_value(e,buffer_arg,1); else emit_m4_writer(e,writer_arg,0); fputs(", ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } + if (!is_print && !is_sprint) fputc(')',e->out); + first=0; + } + verb=(j==i+1) ? ' ' : (j==i+2 ? (unsigned char)fmt[i+1] : '?'); + if(arg) { + error_value=arg->sem_type && arg->sem_type->kind==FE_TYPE_ENUM && + arg->sem_type->is_error; + if(!first) fputs(", ",e->out); + if (!is_print && !is_sprint) + fputs("fe_m4_error ? fe_m4_error : (fe_m4_error = ",e->out); + if(is_print) emit_m4_arg(e,arg,verb,0,0,error_value); + else { + /* The writer expression is repeated intentionally; it is + a value wrapper and does not re-evaluate source args. */ + emit_m4_arg(e,arg,verb, is_sprint ? buffer_arg : writer_arg, + is_sprint,error_value); + } + if (!is_print && !is_sprint) fputc(')',e->out); + first=0; arg=arg->next; ++count; + } + last=j+1; i=j+1; continue; + } + ++i; + } + if(fmt[i]=='"' && i>last) { + if(!first) fputs(", ",e->out); + if (!is_print && !is_sprint) + fputs("fe_m4_error ? fe_m4_error : (fe_m4_error = ",e->out); + if(is_print) { fputs("fe_m4_write_cstr(fe_m4_stdout_writer(), ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } + else { fputs("fe_m4_write_cstr(",e->out); if(is_sprint) emit_m4_writer_value(e,buffer_arg,1); else emit_m4_writer(e,writer_arg,0); fputs(", ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } + if (!is_print && !is_sprint) fputc(')',e->out); + first=0; + } + if(first) fputs("0",e->out); + if(is_print) fputs(", (void)0",e->out); + else if(is_sprint) { fputs(", fe_m4_sprint_finish()",e->out); } + fputc(')',e->out); + (void)count; +} + static void emit_lvalue(FeEmitter *e, FeNode *n) { FeType *bt; @@ -300,14 +525,23 @@ static void emit_slice_call(FeEmitter *e, FeNode *n) FeType *bt=n->a ? n->a->sem_type : 0; const char *maker=bt && bt->slicer ? bt->slicer : "fe_slice_str"; if (!n->b && !n->c && bt && bt->full_slicer) { - fputs(bt->full_slicer,e->out); fputc('(',e->out); emit_expr(e,n->a); fputc(')',e->out); return; + fputs(bt->full_slicer,e->out); + if (bt->kind==FE_TYPE_ARRAY) { fputs("(&",e->out); emit_lvalue(e,n->a); } + else { fputc('(',e->out); emit_expr(e,n->a); } + fputc(')',e->out); return; } if (!n->c && bt && bt->tail_slicer) { - fputs(bt->tail_slicer,e->out); fputc('(',e->out); emit_expr(e,n->a); fputs(", ",e->out); + fputs(bt->tail_slicer,e->out); + if (bt->kind==FE_TYPE_ARRAY) { fputs("(&",e->out); emit_lvalue(e,n->a); } + else { fputc('(',e->out); emit_expr(e,n->a); } + fputs(", ",e->out); if (n->b) emit_expr(e,n->b); else fputs("0",e->out); fputc(')',e->out); return; } - fputs(maker,e->out); fputc('(',e->out); emit_expr(e,n->a); fputs(", ",e->out); + fputs(maker,e->out); + if (bt && bt->kind==FE_TYPE_ARRAY) { fputs("(&",e->out); emit_lvalue(e,n->a); } + else { fputc('(',e->out); emit_expr(e,n->a); } + fputs(", ",e->out); if(n->b) emit_expr(e,n->b); else fputs("0",e->out); fputs(", ",e->out); if(n->c) emit_expr(e,n->c); @@ -375,6 +609,7 @@ static void emit_expr(FeEmitter *e, FeNode *n) } case FE_N_UNARY: op = n->text ? n->text : ""; + if (strcmp(op, "try") == 0) { emit_expr(e,n->a); break; } if (strcmp(op, "not") == 0) fputs("(!", e->out); else { fputc('(', e->out); @@ -405,7 +640,16 @@ static void emit_expr(FeEmitter *e, FeNode *n) case FE_N_CALL: { FeVariantType *v; int special=0; - if(!n->a && n->text && strcmp(n->text,"@size_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%lu",fe_type_size(fe_type_intern(&e->check->types,n->children->text))); special=1; } + if(n->text && (strcmp(n->text,"@print")==0 || strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { emit_m4_builtin(e,n); special=1; } + else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"buf_writer")==0 && n->children) { emit_m4_writer(e,n->children,1); special=1; } + else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"null_writer")==0) { fputs("fe_m4_null_writer()",e->out); special=1; } + else if(!n->a && n->text && strcmp(n->text,"@size_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%lu",fe_type_size(fe_type_intern(&e->check->types,n->children->text))); special=1; } else if(!n->a && n->text && strcmp(n->text,"@align_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%u",fe_type_align(fe_type_intern(&e->check->types,n->children->text))); special=1; } else if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM) { v=fe_type_variant(n->a->a->sem_type,n->a->b ? n->a->b->text : ""); @@ -424,7 +668,10 @@ static void emit_expr(FeEmitter *e, FeNode *n) } case FE_N_MEMBER: { FeVariantType *v; - if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && + if(n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"io")==0 && n->b && n->b->text && + strcmp(n->b->text,"stdout")==0) fputs("fe_m4_stdout_writer()",e->out); + else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && n->b && n->b->text && strcmp(n->b->text,"^")==0) { fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); } else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_ENUM) { v=fe_type_variant(n->a->sem_type,n->b ? n->b->text : ""); if(v) fputs(v->maker,e->out); else fputs("0",e->out); if(v)fputs("()",e->out); } @@ -443,6 +690,16 @@ static void emit_decl(FeEmitter *e, FeNode *n) fputs(ctype(e, n), e->out); fputc(' ', e->out); fputs(cname(n, "fe_local"), e->out); + if (n->kind==FE_N_CONST && n->b) { + fputs(" = ",e->out); + if (n->b->kind==FE_N_LITERAL && n->b->text && n->b->text[0]=='"') { + fputs("{ (const unsigned char*)",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(", sizeof(",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(")-1 }",e->out); + } else emit_expr(e,n->b); + } fputs(";\n", e->out); } @@ -463,9 +720,15 @@ static void emit_block(FeEmitter *e, FeNode *n) ++e->indent; /* C89 requires declarations before statements in each actual block. */ for (x = n->children; x; x = x->next) - if (x->kind == FE_N_LET || x->kind == FE_N_VAR) emit_decl(e, x); + if (x->kind == FE_N_LET || x->kind == FE_N_VAR || + x->kind == FE_N_CONST) emit_decl(e, x); for (x = n->children; x; x = x->next) emit_stmt(e, x); --e->indent; + if (e->fallthrough_block==n) { + pad(e); + fputs("return 0;\n",e->out); + e->fallthrough_block=0; + } pad(e); fputc('}', e->out); } @@ -531,8 +794,15 @@ static void emit_stmt(FeEmitter *e, FeNode *n) break; case FE_N_EXPR_STMT: pad(e); - emit_expr(e, n->a); - fputs(";\n", e->out); + if (n->a && n->a->kind==FE_N_UNARY && n->a->text && + strcmp(n->a->text,"try")==0 && n->a->a) { + fputs("if ((fe_m4_error = ",e->out); + emit_expr(e,n->a->a); + fputs(") != 0) return fe_m4_error;\n",e->out); + } else { + emit_expr(e, n->a); + fputs(";\n", e->out); + } break; case FE_N_RETURN: pad(e); @@ -630,6 +900,10 @@ static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) if (prototype) fputs(";\n", e->out); else { fputs(" ", e->out); + if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && + fn->sem_type->error_value && + fn->sem_type->error_value->kind==FE_TYPE_VOID) + e->fallthrough_block=fn->c; emit_block(e, fn->c); fputc('\n', e->out); } @@ -658,19 +932,26 @@ void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, e->indent = 0; e->no_checks = no_checks; e->temp_serial = 0; + e->fallthrough_block = 0; } void fe_emit_c_program(FeEmitter *e) { FeNode *n; FeNode *main_fn = 0; - fputs("/* generated by fec M3 */\n#include \n#include \ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out); + FeType *type; + int need_m4; + need_m4=node_uses_m4(e->check->ast->root); + for (type=e->check->types.types; type; type=type->next) + if (strcmp(type->name,"io.Writer")==0) need_m4=1; + fputs("/* generated by fec M4 */\n#include \n#include \n#include \n#include \ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out); if (e->pointer_bits==16) fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); else fputs("typedef char fe_assert_usize[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); fputs("static void fe_trap_bounds(void) { abort(); }\n\n", e->out); emit_type_defs(e); + if (need_m4) emit_m4_runtime(e); emit_type_helpers(e); for (n = e->check->ast->root ? e->check->ast->root->children : 0; n; n = n->next) { @@ -680,7 +961,14 @@ void fe_emit_c_program(FeEmitter *e) fputs(cname(n, "fe_global"), e->out); if (n->b) { fputs(" = ", e->out); - emit_expr(e, n->b); + if (n->kind==FE_N_CONST && n->b->kind==FE_N_LITERAL && + n->b->text && n->b->text[0]=='"') { + fputs("{ (const unsigned char*)",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(", sizeof(",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(")-1 }",e->out); + } else emit_expr(e, n->b); } fputs(";\n", e->out); } diff --git a/fec/src/emit_c.h b/fec/src/emit_c.h index b9f12ab..bc927a5 100644 --- a/fec/src/emit_c.h +++ b/fec/src/emit_c.h @@ -10,6 +10,7 @@ typedef struct FeEmitter { int indent; int no_checks; unsigned temp_serial; + FeNode *fallthrough_block; } FeEmitter; void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, diff --git a/fec/src/parser.c b/fec/src/parser.c index f5abc90..9ddd72c 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -58,7 +58,14 @@ static FeNode *type(FeParser *p) } if (is_name(p) || is(p,FE_TOK_TYPE)) { next(p); n=toknode(p,FE_N_TYPE,t); - if (eat(p,FE_TOK_DOT)) { if(is_name(p)){FeNode *m=toknode(p,FE_N_IDENT,p->previous); n->a=m; next(p);} else error(p,"expected type name after '.'"); } + if (eat(p,FE_TOK_DOT)) { + if(is_name(p)) { + FeToken mt=p->current; + FeNode *m=toknode(p,FE_N_IDENT,mt); + n->a=m; + next(p); + } else error(p,"expected type name after '.'"); + } if (eat(p,FE_TOK_BANG)) { FeNode *e=toknode(p,FE_N_TYPE,p->previous); e->a=n; e->b=type(p); return e; } if (eat(p,FE_TOK_LPAREN)) { while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' in generic type"); } return n; @@ -147,7 +154,7 @@ static FeNode *postfix(FeParser *p) static FeNode *expr(FeParser *p, int minprec) { FeToken t=p->current; FeNode *left,*n; int prec; - if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); n->a=expr(p,11); left=n; } + if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; } else left=postfix(p); for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break;next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; } return left; @@ -229,7 +236,7 @@ static FeNode *statement(FeParser *p) if(is(p,FE_TOK_LBRACE)) return block(p); if(eat(p,FE_TOK_LET)) { n=toknode(p,FE_N_LET,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in let");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_VAR)) { n=toknode(p,FE_N_VAR,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);if(eat(p,FE_TOK_EQ))n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } - if(eat(p,FE_TOK_CONST)) { n=toknode(p,FE_N_CONST,t);if(is_name(p))next(p);else error(p,"expected constant name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in const");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } + if(eat(p,FE_TOK_CONST)) { n=toknode(p,FE_N_CONST,t);if(is_name(p)){n->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected constant name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in const");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_IF)) { n=toknode(p,FE_N_IF,t);if(eat(p,FE_TOK_LET)){n->text=fe_arena_strdup(&p->ast->arena,"if let",6);if(is_name(p))next(p);if(eat(p,FE_TOK_LPAREN)){if(is_name(p))next(p);want(p,FE_TOK_RPAREN,"expected ')' in if let pattern");}want(p,FE_TOK_EQ,"expected '=' in if let");}n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } if(eat(p,FE_TOK_COMPTIME)) { n=toknode(p,FE_N_IF,t);want(p,FE_TOK_IF,"expected 'if' after comptime");n->text=fe_arena_strdup(&p->ast->arena,"comptime if",11);n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } if(eat(p,FE_TOK_WHILE)) {n=toknode(p,FE_N_WHILE,t);n->a=header_expr(p);n->b=block(p);return n;} diff --git a/fec/src/types.c b/fec/src/types.c index 8640ac1..3d77341 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -22,10 +22,12 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->bits = 0; t->is_unsigned = 0; t->packed = 0; + t->is_error = 0; t->length = 0; t->size = 0; t->align = 1; t->elem = 0; + t->error_value = 0; t->ref_mut = 0; t->fields = 0; t->field_count = 0; @@ -60,6 +62,9 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) else if (strcmp(name, "bool") == 0) kind = FE_TYPE_BOOL; else if (strcmp(name, "char") == 0) kind = FE_TYPE_CHAR; else if (strcmp(name, "str") == 0) kind = FE_TYPE_STR; + else if (strcmp(name, "io.Writer") == 0) { + kind = FE_TYPE_STRUCT; + } else if (strcmp(name, "i8") == 0 || strcmp(name, "u8") == 0) { kind = FE_TYPE_INT; bits = 8; uns = name[0] == 'u'; } else if (strcmp(name, "i16") == 0 || strcmp(name, "u16") == 0) { @@ -73,6 +78,12 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) if (!t) return 0; t->bits = bits; t->is_unsigned = uns; + if (strcmp(name,"io.Writer")==0) { + t->cname=fe_arena_strdup(ctx->arena,"fe_writer",10); + t->size=4; + t->align=1; + return t; + } if (kind == FE_TYPE_STR) { t->cname = fe_arena_strdup(ctx->arena, "fe_str", 6); t->elem = fe_type_intern(ctx, "u8"); @@ -155,6 +166,19 @@ FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) return t; } +FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value) +{ + char key[128]; + FeType *t; + sprintf(key,"!%s",value ? value->name : "?"); + t=fe_type_intern(ctx,key); + if(t->kind==FE_TYPE_UNKNOWN) { + t->kind=FE_TYPE_ERROR_UNION; + t->error_value=value; + } + return t; +} + FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) { FeType *t; @@ -227,7 +251,10 @@ FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node) t->variants[i].name = v->text; t->variants[i].fields = 0; t->variants[i].field_count = 0; - t->variants[i].tag = i; + if (node->kind==FE_N_ERROR_DECL && v->a && + v->a->kind==FE_N_LITERAL && v->a->text) + t->variants[i].tag=(unsigned)strtoul(v->a->text,0,0); + else t->variants[i].tag = i; t->variants[i].ast_node = v; t->variants[i].maker = generated_name(ctx, "fe_make_variant_", v->text ? v->text : "variant"); if (v->a && v->a->kind == FE_N_TYPE) { @@ -266,6 +293,13 @@ FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node) return t; } +FeType *fe_type_declare_error(FeTypeCtx *ctx, const FeNode *node) +{ + FeType *t=fe_type_declare_enum(ctx,node); + if (t) t->is_error=1; + return t; +} + static unsigned long round_up(unsigned long x, unsigned a) { unsigned long rem; @@ -302,6 +336,10 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) t->cycle_state = 1; if (t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->cycle_state = 2; return; } + if (t->kind == FE_TYPE_ERROR_UNION) { + t->size = 2; t->align = ctx->pointer_bits == 16 ? 1U : 2U; + t->cycle_state = 2; return; + } if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) { t->size = 1; t->align = 1; t->cycle_state = 2; return; } @@ -395,12 +433,18 @@ FeVariantType *fe_type_variant(FeType *t, const char *name) FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) { unsigned long length = 0; + char qualified[128]; if (!node) return fe_type_intern(ctx, ""); if (node->kind != FE_N_TYPE) return fe_type_intern(ctx, ""); if (node->text && strcmp(node->text, "as") == 0) return fe_type_from_ast(ctx, node->b); if (node->text && strcmp(node->text, "str") == 0) return fe_type_intern(ctx, "str"); + if (node->a && node->a->kind==FE_N_IDENT && node->text && + strcmp(node->text,"io")==0 && node->a->text) { + sprintf(qualified,"%s.%s",node->text,node->a->text); + return fe_type_intern(ctx,qualified); + } if (node->text && (strcmp(node->text, "&") == 0 || strcmp(node->text, "&mut") == 0)) return fe_type_ref(ctx, fe_type_from_ast(ctx,node->a), @@ -413,8 +457,12 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) } return fe_type_slice(ctx, fe_type_from_ast(ctx, node->b)); } + if (node->text && strcmp(node->text, "!") == 0) + /* Prefix !T stores T in a; the E!T spelling stores its success + type in b and the error type in a. */ + return fe_type_error_union(ctx, fe_type_from_ast( + ctx, node->b ? node->b : node->a)); if (node->text && (strcmp(node->text, "?") == 0 || - strcmp(node->text, "!") == 0 || strcmp(node->text, "^") == 0 || strcmp(node->text, "&") == 0 || strcmp(node->text, "&mut") == 0 || @@ -447,6 +495,7 @@ const char *fe_type_c_name(const FeType *t, unsigned pointer_bits) if (!t) return "long"; if (t->cname) return t->cname; if (t->kind == FE_TYPE_VOID) return "void"; + if (t->kind == FE_TYPE_ERROR_UNION) return "unsigned short"; if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) return "unsigned char"; if (t->kind == FE_TYPE_REF) { static char ref_name[128]; diff --git a/fec/src/types.h b/fec/src/types.h index 5165c78..16fa67e 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -4,7 +4,7 @@ #include "ast.h" typedef enum FeTypeKind { - FE_TYPE_ERROR, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, + FE_TYPE_ERROR, FE_TYPE_ERROR_UNION, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, FE_TYPE_STRUCT, FE_TYPE_ENUM, FE_TYPE_ARRAY, FE_TYPE_SLICE, FE_TYPE_STR, FE_TYPE_REF, FE_TYPE_UNKNOWN } FeTypeKind; @@ -40,10 +40,13 @@ struct FeType { unsigned bits; int is_unsigned; int packed; + int is_error; unsigned long length; unsigned long size; unsigned align; FeType *elem; + /* Success value for an error union; !void is represented directly. */ + FeType *error_value; int ref_mut; FeFieldType *fields; unsigned field_count; @@ -68,8 +71,10 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node); FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem); FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable); +FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value); FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed); FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node); +FeType *fe_type_declare_error(FeTypeCtx *ctx, const FeNode *node); void fe_type_layout_all(FeTypeCtx *ctx); FeFieldType *fe_type_field(FeType *t, const char *name); FeVariantType *fe_type_variant(FeType *t, const char *name); diff --git a/fec/test-dos.bat b/fec/test-dos.bat index 22ce1f9..acc08bb 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -189,6 +189,45 @@ if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M3\BADINDEX.FE -o TESTS\M3\BADINDEX.C > nul if not errorlevel 1 goto test_fail +if exist TESTS\M4\FORMAT.C del TESTS\M4\FORMAT.C +if exist TESTS\M4\FORMAT.EXE del TESTS\M4\FORMAT.EXE +fec.exe --target=bits32 --emit-c TESTS\M4\FORMAT.FE -o TESTS\M4\FORMAT.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\FORMAT.EXE TESTS\M4\FORMAT.C +if errorlevel 1 goto test_fail +TESTS\M4\FORMAT.EXE > nul +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\TRY-FPR.FE -o TESTS\M4\TRY-FPR.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\TRY-FPR.EXE TESTS\M4\TRY-FPR.C +if errorlevel 1 goto test_fail +TESTS\M4\TRY-FPR.EXE > nul +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\PROP.FE -o TESTS\M4\PROP.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\PROP.EXE TESTS\M4\PROPTEST.C +if errorlevel 1 goto test_fail +TESTS\M4\PROP.EXE > nul +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-ARI.FE -o TESTS\M4\BAD-ARI.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-VERB.FE -o TESTS\M4\BAD-VERB.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-RUN.FE -o TESTS\M4\BAD-RUN.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TYP.FE -o TESTS\M4\BAD-TYP.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TRY.FE -o TESTS\M4\BAD-TRY.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-WRI.FE -o TESTS\M4\BAD-WRI.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-MANY.FE -o TESTS\M4\BAD-MANY.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-OPEN.FE -o TESTS\M4\BAD-OPEN.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-CLS.FE -o TESTS\M4\BAD-CLS.C > nul +if not errorlevel 1 goto test_fail + echo OK>TEST.OK cd C:\FEC goto test_done diff --git a/fec/tests/m4/bad-arity.fe b/fec/tests/m4/bad-arity.fe new file mode 100644 index 0000000..3b29ff8 --- /dev/null +++ b/fec/tests/m4/bad-arity.fe @@ -0,0 +1,6 @@ +unit m4_bad_arity; + +fn main() -> i32 { + @print("{} {}", 1); + return 0; +} diff --git a/fec/tests/m4/bad-cls.fe b/fec/tests/m4/bad-cls.fe new file mode 100644 index 0000000..a321e1c --- /dev/null +++ b/fec/tests/m4/bad-cls.fe @@ -0,0 +1,6 @@ +unit m4_bad_cls; + +fn main() -> i32 { + @print("}", 1); + return 0; +} diff --git a/fec/tests/m4/bad-many.fe b/fec/tests/m4/bad-many.fe new file mode 100644 index 0000000..66a7833 --- /dev/null +++ b/fec/tests/m4/bad-many.fe @@ -0,0 +1,6 @@ +unit m4_bad_many; + +fn main() -> i32 { + @print("{}", 1, 2); + return 0; +} diff --git a/fec/tests/m4/bad-open.fe b/fec/tests/m4/bad-open.fe new file mode 100644 index 0000000..3f1f584 --- /dev/null +++ b/fec/tests/m4/bad-open.fe @@ -0,0 +1,6 @@ +unit m4_bad_open; + +fn main() -> i32 { + @print("{", 1); + return 0; +} diff --git a/fec/tests/m4/bad-runtime.fe b/fec/tests/m4/bad-runtime.fe new file mode 100644 index 0000000..fa6d7f1 --- /dev/null +++ b/fec/tests/m4/bad-runtime.fe @@ -0,0 +1,7 @@ +unit m4_bad_runtime; + +fn main() -> i32 { + var fmt: str = "{}"; + @print(fmt, 1); + return 0; +} diff --git a/fec/tests/m4/bad-try.fe b/fec/tests/m4/bad-try.fe new file mode 100644 index 0000000..2bd0e28 --- /dev/null +++ b/fec/tests/m4/bad-try.fe @@ -0,0 +1,6 @@ +unit m4_bad_try; + +fn main() -> i32 { + try @print("nope"); + return 0; +} diff --git a/fec/tests/m4/bad-type.fe b/fec/tests/m4/bad-type.fe new file mode 100644 index 0000000..e88b48f --- /dev/null +++ b/fec/tests/m4/bad-type.fe @@ -0,0 +1,9 @@ +unit m4_bad_type; + +struct Point { x: i32, } + +fn main() -> i32 { + let p: Point = Point{ x: 1 }; + @print("{}", p); + return 0; +} diff --git a/fec/tests/m4/bad-verb.fe b/fec/tests/m4/bad-verb.fe new file mode 100644 index 0000000..ea19d9a --- /dev/null +++ b/fec/tests/m4/bad-verb.fe @@ -0,0 +1,6 @@ +unit m4_bad_verb; + +fn main() -> i32 { + @print("{q}", 1); + return 0; +} diff --git a/fec/tests/m4/bad-writer.fe b/fec/tests/m4/bad-writer.fe new file mode 100644 index 0000000..8dac8c4 --- /dev/null +++ b/fec/tests/m4/bad-writer.fe @@ -0,0 +1,7 @@ +unit m4_bad_writer; + +fn main() -> i32 { + var x: i32 = 0; + @fprint(&mut x, "bad"); + return 0; +} diff --git a/fec/tests/m4/format.fe b/fec/tests/m4/format.fe new file mode 100644 index 0000000..e3582fd --- /dev/null +++ b/fec/tests/m4/format.fe @@ -0,0 +1,30 @@ +unit m4_format; + +const FMT: str = "n={} hex={x} c={c} s={s} b={b} {{ok}}\n"; + +fn main() -> i32 { + var raw: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw2: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw3: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw4: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var raw5: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; + var buf: []u8 = raw[..]; + var buf2: []u8 = raw2[..]; + var buf3: []u8 = raw3[..]; + var buf4: []u8 = raw4[..]; + var buf5: []u8 = raw5[..]; + let w: io.Writer = io.buf_writer(&mut buf); + const LOCAL_FMT: str = "value={}\n"; + @print(FMT, 7, 15, 'A', "yes", true); + @fprint(&mut w, LOCAL_FMT, 12); + let n: usize = @sprint(buf2, "A\x42\u0043defghi"); + let n2: usize = @sprint(buf3, "xy"); + let inner_n: usize = @sprint(buf4, "xy"); + let outer_n: usize = @sprint(buf5, "n={}", @sprint(buf4, "xy")); + if n == 8 and buf2[0] == ('A' as u8) and + buf2[1] == ('B' as u8) and buf2[2] == ('C' as u8) and + n2 == 2 and buf3[0] == ('x' as u8) and + inner_n == 2 and outer_n == 3 and + buf5[0] == ('n' as u8) and buf5[2] == ('2' as u8) { return 0; } + return 1; +} diff --git a/fec/tests/m4/prop.fe b/fec/tests/m4/prop.fe new file mode 100644 index 0000000..81a85f4 --- /dev/null +++ b/fec/tests/m4/prop.fe @@ -0,0 +1,5 @@ +unit m4_prop; + +pub fn propagate(w: &mut io.Writer) -> !void { + try @fprint(w, "a{}b", 1); +} diff --git a/fec/tests/m4/proptest.c b/fec/tests/m4/proptest.c new file mode 100644 index 0000000..7261461 --- /dev/null +++ b/fec/tests/m4/proptest.c @@ -0,0 +1,23 @@ +#include "prop.c" + +static unsigned short calls; + +static unsigned short fail_write(void *ctx, const unsigned char *p, + unsigned long n) +{ + (void)ctx; + (void)p; + (void)n; + ++calls; + return calls == 1 ? 7 : 0; +} + +int main(void) +{ + fe_writer w; + unsigned short result; + w.ctx=0; + w.write_fn=fail_write; + result=fe_m4_prop_propagate(&w); + return (result==7 && calls==1) ? 0 : 1; +} diff --git a/fec/tests/m4/try-fprint.fe b/fec/tests/m4/try-fprint.fe new file mode 100644 index 0000000..e6c419c --- /dev/null +++ b/fec/tests/m4/try-fprint.fe @@ -0,0 +1,8 @@ +unit m4_try_fprint; + +fn main() -> !void { + var raw: [4]u8 = [0, 0, 0, 0]; + var buf: []u8 = raw[..]; + let w: io.Writer = io.buf_writer(&mut buf); + try @fprint(&mut w, "ok"); +} diff --git a/fec/tests/run-tests.sh b/fec/tests/run-tests.sh index 9b907cc..5d211ac 100644 --- a/fec/tests/run-tests.sh +++ b/fec/tests/run-tests.sh @@ -61,3 +61,33 @@ for f in badfld badmat badarr badcycle badstr badchar badfield badindex; do fi done echo "M3 tests: structs, enums, arrays, slices, str, match, and bounds passed" + +m4tmp=$(mktemp -d) +trap 'rm -rf "$m2tmp" "$m3tmp" "$m4tmp"' EXIT HUP INT TERM +for f in format; do + "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" + ${CC:-cc} -std=c89 -pedantic "$m4tmp/$f.c" -o "$m4tmp/$f" + "$m4tmp/$f" >"$m4tmp/$f.out" +done +for f in try-fprint; do + "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" + ${CC:-cc} -std=c89 -pedantic "$m4tmp/$f.c" -o "$m4tmp/$f" + "$m4tmp/$f" +done +"$root"/fec --target=bits32 --emit-c "$root"/tests/m4/prop.fe -o "$m4tmp/prop.c" +cp "$root"/tests/m4/proptest.c "$m4tmp/proptest.c" +${CC:-cc} -std=c89 -pedantic "$m4tmp/proptest.c" -o "$m4tmp/prop" +"$m4tmp/prop" +for f in bad-arity bad-verb bad-runtime bad-type bad-try bad-writer; do + if "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" >/dev/null 2>/dev/null; then + echo "FAIL (accepted M4 semantic error): $f.fe" + exit 1 + fi +done +for f in bad-many bad-open bad-cls; do + if "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" >/dev/null 2>/dev/null; then + echo "FAIL (accepted M4 format-brace error): $f.fe" + exit 1 + fi +done +echo "M4 tests: formatting builtins passed" diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat index 63f9cb7..b5755f1 100644 --- a/fec/vm-m1.bat +++ b/fec/vm-m1.bat @@ -8,6 +8,7 @@ if not exist C:\FEC\TESTS\PASS md C:\FEC\TESTS\PASS if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL if not exist C:\FEC\TESTS\M2 md C:\FEC\TESTS\M2 if not exist C:\FEC\TESTS\M3 md C:\FEC\TESTS\M3 +if not exist C:\FEC\TESTS\M4 md C:\FEC\TESTS\M4 if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL if exist C:\FEC\STAGE.FAIL del C:\FEC\STAGE.FAIL @@ -142,6 +143,33 @@ if errorlevel 1 goto stage_fail copy D:\FEC\TESTS\M3\BADINDEX.FE C:\FEC\TESTS\M3\BADINDEX.FE > nul if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\FORMAT.FE C:\FEC\TESTS\M4\FORMAT.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-ARI~1.FE C:\FEC\TESTS\M4\BAD-ARI.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-VERB.FE C:\FEC\TESTS\M4\BAD-VERB.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-RUN~1.FE C:\FEC\TESTS\M4\BAD-RUN.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-TYP~1.FE C:\FEC\TESTS\M4\BAD-TYP.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-TRY.FE C:\FEC\TESTS\M4\BAD-TRY.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\TRY-FPR~1.FE C:\FEC\TESTS\M4\TRY-FPR.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-WRI~1.FE C:\FEC\TESTS\M4\BAD-WRI.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\PROP.FE C:\FEC\TESTS\M4\PROP.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\PROPTEST.C C:\FEC\TESTS\M4\PROPTEST.C > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-MANY.FE C:\FEC\TESTS\M4\BAD-MANY.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-OPEN.FE C:\FEC\TESTS\M4\BAD-OPEN.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M4\BAD-CLS.FE C:\FEC\TESTS\M4\BAD-CLS.FE > nul +if errorlevel 1 goto stage_fail + call C:\FEC\TEST-DOS.BAT if exist C:\FEC\TEST.OK goto vm_success echo FAIL>C:\FEC\VM.FAIL From 6acfc5918d44790bda58328874d778aa7f7517f7 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 13:35:41 +0900 Subject: [PATCH 011/184] docs: add implementation handoff --- HANDOFF.md | 212 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 0000000..8de2256 --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,212 @@ +# doslang 작업 인수인계 + +작성 시각: 2026-08-16 (Asia/Seoul) + +## 최종 목표와 사용자 지시 + +- `SPEC.md`의 최신 명세대로 M14까지 전부 구현한다. +- DOSBox는 사용하지 않는다. 실행과 권위 있는 검증은 **QEMU FreeDOS 내부**에서만 한다. +- 컴파일러 A와 생성 C 모두 VM 내부 Open Watcom으로 컴파일한다. + - 컴파일러 A / bits16: `WCL` + - bits32 생성 C: `WCL386` +- 호스트 Windows는 소스 편집, diff, Git, 직렬 전송에만 사용한다. +- WSL이나 호스트 C 컴파일러 결과를 정식 검증으로 인정하지 않는다. +- 코딩은 가능한 한 `gpt-5.6-luna` 서브에이전트에 맡기고, 루트 에이전트는 명세 감사와 QEMU 검증을 담당한다. +- VGA 데모처럼 복잡한 멀티모달 수동 검증은 완료 게이트에서 제외한다. +- 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. +- 구현 중 명세 판단이 바뀌면 `SPEC.md`를 즉시 최신화하고 `SPEC.AUDIT.md`에 변경 이유를 누적한다. + +## 현재 완료 상태 + +현재 기준 커밋과 원격 `master`는 둘 다 다음 SHA다. + +```text +53bca214b82c3abf7882ad41fb177cc488e999af +``` + +완료 및 QEMU/Open Watcom 검증된 범위: + +- M1: lexer, parser, AST dump +- M2: 기본 타입 검사와 C 방출, bits16/bits32 경로 +- M3: struct, enum, match, 배열, 슬라이스, str, 경계 검사, 반복 참조 +- M4: `@print`, `@fprint`, `@sprint`, 최소 `io.Writer` + +마지막 완료 커밋: + +```text +53bca21 feat: implement M4 formatting builtins +8e6a409 feat: implement M3 aggregate types and iteration +57b47a5 docs: disambiguate control-flow headers +95de333 docs: disambiguate match scrutinees +3aa7618 docs: clarify char and byte conversions +``` + +M4 최종 검증 증거: + +- VM의 `C:\FEC\BUILD.OK`: `OK` +- VM의 `C:\FEC\TEST.OK`: `OK` +- `C:\FEC\TEST-DOS.BAT`: exit 0 +- 오류 코드 7을 반환하는 C89 Writer harness가 `@fprint`의 정확한 오류 보존과 첫 오류 이후 단축 중단을 실행 검증한다. +- 로컬/원격 SHA 일치까지 확인하고 푸시했다. + +## 최신 명세 + +- `SPEC.md`: v0.1.5 +- `SPEC.AUDIT.md`: 최초 감사 이후 설계 변경 로그 +- 주요 후속 결정: + - `char`와 `u8`은 별개 타입이며 명시적 `as`만 허용한다. + - 제어 흐름 헤더 직후 `{`는 본문 시작이다. 헤더 최상위 구조체 초기화식은 괄호로 구분한다. + +## 현재 워크트리: 미완성 M5 + +M5 작업 도중 인수인계를 위해 Luna를 중단했다. **아래 변경은 미검증·미커밋 상태이므로 버리지 말고 먼저 감사할 것.** + +수정 파일: + +```text +fec/src/check.c +fec/src/emit_c.c +fec/src/emit_c.h +fec/src/types.c +fec/src/types.h +fec/test-dos.bat +fec/tests/run-tests.sh +fec/vm-m1.bat +``` + +새 파일: + +```text +fec/tests/m5/defer.fe +fec/tests/m5/owned.fe +fec/tests/m5/bad-move.fe +fec/tests/m5/bad-destroy.fe +``` + +현재 diff 규모는 약 251 insertions / 6 deletions이며 `git diff --check`는 통과했다. 아직 DOS로 전송하거나 빌드하지 않았다. + +현재 부분 구현에 들어간 것으로 확인된 것: + +- `FE_TYPE_OWNED` +- struct의 `has_drop` 메타데이터 일부 +- 단순 `moved` 비트 기반 이동 후 사용 진단 +- `mem.create` / `mem.destroy` 일부 checker/emitter 분기 +- owned drop 및 block cleanup helper +- return/break/continue 정리 경로를 위한 emitter 코드 일부 +- 정상 종료 defer 역순 방출 일부 + +그러나 M5 완료로 간주하면 안 된다. 첫 Luna 결과는 골격뿐이었고 다음 누락 때문에 반려했다. + +- 자동 drop/free가 모든 경로에서 정확히 한 번 실행되는지 +- `mem.create(T) -> !^T`의 실제 C ABI와 초기화 +- 재대입 전에 기존 owned 값 정리 +- defer와 drop의 선언 위치 기준 역순 병합 +- return / break / continue / try 전파에서 cleanup +- 조건부 이동의 `MaybeMoved` 상태와 런타임 live flag +- struct drop 메서드 및 필드 역순 drop +- 분기/루프 상태 합류 +- 누수와 이중 해제를 세는 런타임 harness +- R1/R3 실패 테스트 최소 수량 + +중단 직전 두 번째 Luna 패스가 cleanup 코드를 더 추가했으므로 위 항목 일부가 코드에 들어갔을 수 있다. 다음 세션은 반드시 `git diff`로 실제 구현을 재감사하고, 테스트가 증명하지 않는 기능은 완료 처리하지 말아야 한다. + +현재는 `own.c/h`가 없고 소유권 로직이 주로 `check.c`/`emit_c.c`에 들어가 있다. 복잡도가 계속 커지면 명세 §11.5대로 `own.c/h`와 cleanup/lower 계층을 분리하는 편이 안전하다. + +## 다음 세션 권장 순서 + +1. `git status --short`, `git diff --check`, M5 diff 전체를 읽는다. +2. Luna 읽기 전용 감사 에이전트로 M5와 R1~R5/§11.5를 대조한다. +3. 기존 Luna 구현 에이전트 또는 새 `gpt-5.6-luna`에게 발견된 누락을 수정시킨다. +4. 최소 다음 테스트를 갖춘다. + - owned 정상 scope 종료 cleanup + - 조기 return cleanup + - return 시 defer 실행 + - 중첩 defer/drop 역순 + - owned 재대입 시 기존 값 cleanup + - 함수 인자 이동 후 사용 실패 + - 조건부 이동 후 사용 실패 및 정확한 drop + - 직접 `.drop()` 호출 실패 + - `mem.destroy` 후 재사용/이중 destroy 실패 + - 할당/해제 카운터로 누수 0, double free 0 +5. 변경 파일과 M5 fixture를 직렬 프로토콜로 VM의 `C:\FEC`에 직접 전송한다. +6. `C:\FEC\TEST-DOS.BAT`를 실행한다. 실패하면 출력/생성 C/정확한 fixture를 좁혀 반복 수정한다. +7. `BUILD.OK`, `TEST.OK`, exit 0을 모두 확인한 뒤에만 M5 커밋 및 `git push origin master`. +8. 이후 M6~M14도 같은 방식으로 진행한다. + +## QEMU와 직렬 에이전트 상태 + +인수인계 작성 시 QEMU는 재시작 없이 살아 있다. + +```text +QEMU PID: 11700 +monitor: 127.0.0.1:4444 +DOS tool/controller: 127.0.0.1:5555 +QEMU serial relay: 127.0.0.1:5556 +observer: 127.0.0.1:5557 +``` + +5555에 ASCII 한 줄 명령을 보내는 프로토콜: + +```text +PING +READ +WRITE T|A +EXEC +``` + +- `PING` 정상 응답: `OK 504F4E47` +- `WRITE`는 안전하게 1024바이트 조각으로 보내면 된다. 첫 조각 `T`, 이후 `A`. +- authoritative workspace는 VM의 `C:\FEC`이다. +- QEMU의 vvfat `D:`는 교환용으로만 취급하고 빌드하지 않는다. 과거 D:에서 빌드하다 QEMU가 rename 처리 오류로 종료된 적이 있다. +- DOS의 8.3 파일명 때문에 긴 fixture는 `BAD-ARI.FE`, `TRY-FPR.FE`처럼 명시적으로 짧은 이름으로 전송해야 한다. + +QEMU monitor helper: + +```powershell +.\.qemu\monitor.ps1 'sendkey ctrl-c' +.\.qemu\screenshot.ps1 +``` + +긴 DOS 배치가 멈추면 QEMU를 재시작하지 말고 먼저 `sendkey ctrl-c`를 사용한다. FreeDOS가 다음 프롬프트를 보이면 monitor로 `y`, `ret`을 보낸다. + +```text +Terminate batch file ... (Yes/No/All)? +``` + +그 뒤 `PING` 복구를 확인한다. + +## 빌드 관련 함정 + +- 컴파일러 A는 16비트 large model로 빌드한다. small model은 메모리 부족이 났다. +- DOS 명령줄 길이 제한 때문에 compiler object link는 `*.obj`를 사용한다. +- 따라서 `fec/build-dos.bat`가 먼저 `C:\FEC\*.obj`를 지워 stale 32-bit test object가 섞이지 않게 한다. +- 생성 C의 보수적 미사용 helper 때문에 M4 Watcom 테스트는 `-wx -wcd=202`를 사용한다. W202만 끄고 다른 경고는 오류로 유지한다. +- 호스트에서 컴파일하지 말 것. 정적 text/diff 검사만 허용한다. +- `.qemu/qemu-screen.png`는 진단용이며 커밋하지 않는다. + +## Git과 저장소 + +```text +origin: https://github.com/sebastianrcnt/doslang.git +branch: master +repository visibility: public +``` + +사용자는 모든 검증 커밋을 항상 원격에 푸시하길 원한다. + +이 인수인계 문서는 별도 문서 커밋으로 푸시하되, 현재 미완성 M5 변경은 그 커밋에 포함하지 않는다. + +## 전체 남은 목표 + +- M5: owned/drop/defer/move — 현재 미완성 +- M6: `&`/`&mut`, R1~R8 전체 borrow checker +- M7: `?T`, `E!T`, try/catch +- M8: unit/import/.fei/분리 컴파일/std 초안 +- M9: 제네릭 및 Ferro `List`/`Map` +- M10: bits16 far/asm/interrupt/shared/atomic/critical (VGA 수동 데모 제외) +- M11: Compiler B를 Ferro로 작성하고 A로 빌드 +- M12: self-host fixpoint `B(B(B)) == B(B)` 후 A 폐기 +- M13: 386 네이티브 백엔드 +- M14: 8086 네이티브 백엔드, Watcom 없이 bits16 빌드 + +M14 전체 요구를 증명하기 전에는 goal을 complete로 표시하지 않는다. From c06bb9aa1c34f75a64f3fac914d656f0317a5537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 14:01:51 +0900 Subject: [PATCH 012/184] feat: add source-aware diagnostics --- fec/src/diag.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fec/src/diag.h b/fec/src/diag.h index 6211e84..300f90f 100644 --- a/fec/src/diag.h +++ b/fec/src/diag.h @@ -12,10 +12,14 @@ typedef struct FeLoc { typedef struct FeDiags { unsigned long errors; unsigned long warnings; + const char *source; + unsigned long source_len; } FeDiags; +void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len); void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg); void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg); void fe_diag_note(FeLoc loc, const char *msg); +void fe_diag_note_src(FeDiags *d, FeLoc loc, const char *msg); #endif From f5cf934f8652736b3854eacc191797ef52e60e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 14:02:01 +0900 Subject: [PATCH 013/184] feat: render diagnostic source excerpts --- fec/src/diag.c | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/fec/src/diag.c b/fec/src/diag.c index 21612ce..c9d35b1 100644 --- a/fec/src/diag.c +++ b/fec/src/diag.c @@ -1,9 +1,62 @@ #include "diag.h" +static unsigned long digits(unsigned long n) +{ + unsigned long count=1; + while(n>=10UL){n/=10UL;++count;} + return count; +} + +static void excerpt(const FeDiags *d, FeLoc loc) +{ + const char *src; + unsigned long len; + unsigned long i=0; + unsigned long line=1; + unsigned long start; + unsigned long end; + unsigned long gutter; + unsigned long col; + char c; + if(!d || !d->source || !d->source_len || !loc.line || !loc.col) return; + src=d->source; + len=d->source_len; + while(ilen) return; + start=i; + end=start; + while(endstart) fwrite(src+start,1,(size_t)(end-start),stderr); + fputc('\n',stderr); + fputs(" ",stderr); + for(i=0;ierrors=0; + d->warnings=0; + d->source=source; + d->source_len=source_len; +} + void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg) { d->errors++; fprintf(stderr, "%s:%lu:%lu: error: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); + excerpt(d,loc); } void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg) @@ -12,9 +65,16 @@ void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg) fprintf(stderr, "%s:%lu:%lu: error: ", loc.file ? loc.file : "", loc.line, loc.col); fprintf(stderr, msg, arg); fputc('\n', stderr); + excerpt(d,loc); } void fe_diag_note(FeLoc loc, const char *msg) { fprintf(stderr, "%s:%lu:%lu: note: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); } + +void fe_diag_note_src(FeDiags *d, FeLoc loc, const char *msg) +{ + fe_diag_note(loc,msg); + excerpt(d,loc); +} From 831da3a74fd944c92b0c8908fdd70bfc7acabb9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 14:02:21 +0900 Subject: [PATCH 014/184] feat: add token dump and check modes --- fec/src/driver.c | 97 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 87 insertions(+), 10 deletions(-) diff --git a/fec/src/driver.c b/fec/src/driver.c index bfaa518..3c3fc20 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -13,19 +13,96 @@ static char *read_file(const char *name, unsigned long *size) p=(char *)malloc((unsigned long)n+1); if(!p){fclose(f);return 0;} if(n && fread(p,1,(size_t)n,f)!=(size_t)n){free(p);fclose(f);return 0;} fclose(f);p[n]='\0';*size=(unsigned long)n;return p; } + static void usage(void) -{ puts("usage: fec [--dump-ast|--emit-c] file.fe [--target=bits16|bits32] [-o output.c]"); } +{ + puts("usage: fec [--dump-tokens|--dump-ast|--check|--emit-c] file.fe [--target=bits16|bits32] [-o output.c]"); +} + +static void dump_tokens(const char *src, unsigned long n, const char *file, + FeDiags *d) +{ + FeLexer lexer; + FeToken tok; + fe_lexer_init(&lexer,src,n,file,d); + do { + tok=fe_lexer_next(&lexer); + fprintf(stdout,"%lu:%lu\t%s\t",tok.loc.line,tok.loc.col, + fe_token_name(tok.kind)); + if(tok.length) fwrite(tok.begin,1,(size_t)tok.length,stdout); + else fputc('-',stdout); + fputc('\n',stdout); + } while(tok.kind!=FE_TOK_EOF); +} + int main(int argc, char **argv) { - int i,dump=0,emit=0,no_checks=0; const char *file=0,*outname=0; unsigned long n; char *src; FeDiags d; FeAst ast; FeParser p; FeCheck check; FeEmitter emitter; FILE *out; unsigned pointer_bits=32; - (void)emit; + int i,dump=0,dump_tok=0,check_only=0,emit=0,no_checks=0; + const char *file=0,*outname=0; + unsigned long n; + char *src; + FeDiags d; + FeAst ast; + FeParser p; + FeCheck check; + FeEmitter emitter; + FILE *out; + unsigned pointer_bits=32; if(argc<2){usage();return 2;} - for(i=1;i=argc){fprintf(stderr,"fec: -o needs a path\n");return 2;}outname=argv[++i];} else if(strncmp(argv[i],"-o",2)==0 && argv[i][2]) outname=argv[i]+2; else if(strncmp(argv[i],"--target=bits16",15)==0) pointer_bits=16; else if(strncmp(argv[i],"--target=bits32",15)==0) pointer_bits=32; else if(strcmp(argv[i],"--no-checks")==0) no_checks=1; else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} else {fprintf(stderr,"fec: unknown option %s\n",argv[i]);return 2;} } + for(i=1;i=argc){fprintf(stderr,"fec: -o needs a path\n");return 2;}outname=argv[++i];} + else if(strncmp(argv[i],"-o",2)==0 && argv[i][2]) outname=argv[i]+2; + else if(strncmp(argv[i],"--target=bits16",15)==0) pointer_bits=16; + else if(strncmp(argv[i],"--target=bits32",15)==0) pointer_bits=32; + else if(strcmp(argv[i],"--no-checks")==0) no_checks=1; + else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--strip-error-names")==0) { } + else if(argv[i][0]!='-') file=argv[i]; + else if(strcmp(argv[i],"--help")==0){usage();return 0;} + else {fprintf(stderr,"fec: unknown option %s\n",argv[i]);return 2;} + } + if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)+(emit?1:0)>1){ + fprintf(stderr,"fec: choose only one output mode\n"); + return 2; + } if(!file){fprintf(stderr,"fec: no input file\n");return 2;} - src=read_file(file,&n);if(!src)return 2;d.errors=0;d.warnings=0;fe_ast_init(&ast);fe_parser_init(&p,&ast,src,n,file,&d);ast.root=fe_parse_unit(&p); - if(dump) { fe_ast_dump(ast.root,0,stdout); fe_ast_destroy(&ast); free(src); return d.errors?1:0; } - fe_check_init(&check,&ast,&d,pointer_bits,no_checks); if(!fe_check_program(&check)){fe_ast_destroy(&ast);free(src);return 1;} - out=outname?fopen(outname,"w"):stdout; if(!out){fprintf(stderr,"fec: cannot create %s\n",outname);fe_ast_destroy(&ast);free(src);return 2;} - fe_emit_c_init(&emitter,out,&check,pointer_bits,no_checks);fe_emit_c_program(&emitter);if(outname)fclose(out); - fe_ast_destroy(&ast); free(src); return d.errors?1:0; + src=read_file(file,&n); + if(!src)return 2; + fe_diags_init(&d,src,n); + if(dump_tok){ + dump_tokens(src,n,file,&d); + free(src); + return d.errors?1:0; + } + fe_ast_init(&ast); + fe_parser_init(&p,&ast,src,n,file,&d); + ast.root=fe_parse_unit(&p); + if(dump){ + fe_ast_dump(ast.root,0,stdout); + fe_ast_destroy(&ast); + free(src); + return d.errors?1:0; + } + fe_check_init(&check,&ast,&d,pointer_bits,no_checks); + if(!fe_check_program(&check)){ + fe_ast_destroy(&ast); + free(src); + return 1; + } + if(check_only){ + fe_ast_destroy(&ast); + free(src); + return 0; + } + out=outname?fopen(outname,"w"):stdout; + if(!out){fprintf(stderr,"fec: cannot create %s\n",outname);fe_ast_destroy(&ast);free(src);return 2;} + fe_emit_c_init(&emitter,out,&check,pointer_bits,no_checks); + fe_emit_c_program(&emitter); + if(outname)fclose(out); + fe_ast_destroy(&ast); + free(src); + return d.errors?1:0; } From 799c8b0a83ef21c96646096ebebeb1688a5912c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 14:02:33 +0900 Subject: [PATCH 015/184] test: cover compiler cli tooling --- fec/tests/run-cli-tests.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 fec/tests/run-cli-tests.sh diff --git a/fec/tests/run-cli-tests.sh b/fec/tests/run-cli-tests.sh new file mode 100644 index 0000000..ddecda3 --- /dev/null +++ b/fec/tests/run-cli-tests.sh @@ -0,0 +1,26 @@ +#!/bin/sh +set -eu +root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT HUP INT TERM + +"$root"/fec --dump-tokens "$root"/tests/pass/basic.fe >"$tmp/tokens.out" +grep 'unit' "$tmp/tokens.out" >/dev/null +grep 'identifier' "$tmp/tokens.out" >/dev/null +grep 'eof' "$tmp/tokens.out" >/dev/null + +"$root"/fec --check "$root"/tests/m2/hello.fe >"$tmp/check.out" +if [ -s "$tmp/check.out" ]; then + echo "FAIL: --check produced stdout" + exit 1 +fi + +if "$root"/fec --check "$root"/tests/m2/bad-condition.fe >"$tmp/bad.out" 2>"$tmp/diag.out"; then + echo "FAIL: --check accepted invalid input" + exit 1 +fi +grep 'error:' "$tmp/diag.out" >/dev/null +grep '^ [0-9][0-9]* | ' "$tmp/diag.out" >/dev/null +grep ' | .*\^' "$tmp/diag.out" >/dev/null + +echo "CLI tests: token dump, check mode, and source diagnostics passed" From 5316aba51b0466ed823bf0206feebbee418fa025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 14:02:39 +0900 Subject: [PATCH 016/184] test: run cli regression checks --- fec/Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fec/Makefile b/fec/Makefile index 7f74ee8..17c4f66 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -4,7 +4,7 @@ CPPFLAGS ?= -Isrc SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/check.c src/emit_c.c src/driver.c OBJ = $(SRC:.c=.o) -.PHONY: all clean test dos-build +.PHONY: all clean test test-cli dos-build all: fec fec: $(OBJ) @@ -15,6 +15,10 @@ src/%.o: src/%.c test: fec @./tests/run-tests.sh + @sh ./tests/run-cli-tests.sh + +test-cli: fec + @sh ./tests/run-cli-tests.sh dos-build: @echo "Run build-dos.bat inside FreeDOS/Open Watcom." From 631d2ebb481668b3bc9bac191ad87ce28b469f79 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 14:22:03 +0900 Subject: [PATCH 017/184] wip: advance M5 ownership cleanup --- .pi/agents/Explore.md | 36 ++++ fec/src/check.c | 182 +++++++++++++++- fec/src/emit_c.c | 402 +++++++++++++++++++++++++++++++++--- fec/src/emit_c.h | 6 + fec/src/types.c | 55 ++++- fec/src/types.h | 6 +- fec/test-dos.bat | 19 ++ fec/tests/m5/bad-destroy.fe | 5 + fec/tests/m5/bad-move.fe | 8 + fec/tests/m5/defer.fe | 5 + fec/tests/m5/owned.fe | 9 + fec/tests/m5/runtime.c | 9 + fec/tests/m5/runtime.fe | 19 ++ fec/tests/run-tests.sh | 11 + fec/vm-m1.bat | 13 ++ 15 files changed, 752 insertions(+), 33 deletions(-) create mode 100644 .pi/agents/Explore.md create mode 100644 fec/tests/m5/bad-destroy.fe create mode 100644 fec/tests/m5/bad-move.fe create mode 100644 fec/tests/m5/defer.fe create mode 100644 fec/tests/m5/owned.fe create mode 100644 fec/tests/m5/runtime.c create mode 100644 fec/tests/m5/runtime.fe diff --git a/.pi/agents/Explore.md b/.pi/agents/Explore.md new file mode 100644 index 0000000..801991f --- /dev/null +++ b/.pi/agents/Explore.md @@ -0,0 +1,36 @@ +--- +description: "Fast read-only search agent for locating code. Use it to find files by pattern (eg. \"src/components/**/*.tsx\"), grep for symbols or keywords (eg. \"API endpoints\"), or answer \"where is X defined / which files reference Y.\" Do NOT use it for code review, design-doc auditing, cross-file consistency checks, or open-ended analysis — it reads excerpts rather than whole files and will miss content past its read window. When calling, specify search breadth: \"quick\" for a single targeted lookup, \"medium\" for moderate exploration, or \"very thorough\" to search across multiple locations and naming conventions." +display_name: Explore +tools: read, bash, grep, find, ls +model: gpt-5.6-luna +prompt_mode: replace +--- + +# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS +You are a file search specialist. You excel at thoroughly navigating and exploring codebases. +Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools. + +You are STRICTLY PROHIBITED from: +- Creating new files +- Modifying existing files +- Deleting files +- Moving or copying files +- Creating temporary files anywhere, including /tmp +- Using redirect operators (>, >>, |) or heredocs to write to files +- Running ANY commands that change system state + +Use Bash ONLY for read-only operations: ls, git status, git log, git diff, find, cat, head, tail. + +# Tool Usage +- Use the find tool for file pattern matching (NOT the bash find command) +- Use the grep tool for content search (NOT bash grep/rg command) +- Use the read tool for reading files (NOT bash cat/head/tail) +- Use Bash ONLY for read-only operations +- Make independent tool calls in parallel for efficiency +- Adapt search approach based on thoroughness level specified + +# Output +- Use absolute file paths in all references +- Report findings as regular messages +- Do not use emojis +- Be thorough and precise \ No newline at end of file diff --git a/fec/src/check.c b/fec/src/check.c index 9563b18..28d95d9 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -12,6 +12,7 @@ struct FeSym { FeNode *fn; int mutable; int initialized; + int moved; FeNode *decl; }; @@ -22,11 +23,15 @@ struct FeScope { unsigned capacity; }; +static FeSym *find_symbol(FeScope *scope, const char *name); + typedef struct FeCheckerState { FeCheck *c; FeScope *scope; FeScope *globals; FeType *ret; + unsigned loop_depth; + unsigned defer_depth; } FeCheckerState; static FeType *unknown(FeCheck *c) @@ -44,6 +49,41 @@ static int known(FeType *t) return t && t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ERROR; } +static int is_copy_type(FeType *t) +{ + unsigned i; + if (!t) return 1; + if (t->kind==FE_TYPE_OWNED) return 0; + if (t->kind==FE_TYPE_REF) return !t->ref_mut; + if (t->kind==FE_TYPE_ARRAY) return is_copy_type(t->elem); + if (t->kind==FE_TYPE_STRUCT) { + if (t->has_drop) return 0; + for (i=0;ifield_count;i++) if (!is_copy_type(t->fields[i].type)) return 0; + } + if (t->kind==FE_TYPE_ENUM) + for (i=0;ivariant_count;i++) { + unsigned j; + for (j=0;jvariants[i].field_count;j++) + if (!is_copy_type(t->variants[i].fields[j].type)) return 0; + } + return 1; +} + +static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) +{ + FeSym *sym; + if (!n || !t || is_copy_type(t) || n->kind!=FE_N_IDENT) return; + sym=find_symbol(s->scope,n->text ? n->text : ""); + if (sym) { + if (s->defer_depth) { + if (sym->decl) sym->decl->flags |= 0x200U; + } else { + sym->moved=1; + if (sym->decl) sym->decl->flags |= 0x100U; + } + } +} + static int compatible(FeType *want, FeType *got, FeNode *value) { FeNode *item; @@ -193,6 +233,7 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, sym->fn = fn; sym->mutable = mutable; sym->initialized = initialized; + sym->moved = 0; sym->decl = decl; if (decl) { decl->cname = cname; @@ -217,6 +258,47 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n); static void check_match(FeCheckerState *s, FeNode *n); static void check_stmt(FeCheckerState *s, FeNode *n); +typedef struct FeFlowSlot { + FeSym *sym; + int moved; + int initialized; +} FeFlowSlot; + +static unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap) +{ + unsigned count=0; + unsigned i; + FeScope *p; + for (p=scope; p && countparent) + for (i=0; icount && countitems[i]; + slots[count].moved=p->items[i].moved; + slots[count].initialized=p->items[i].initialized; + ++count; + } + return count; +} + +static void flow_restore(FeFlowSlot *slots, unsigned count) +{ + unsigned i; + for (i=0; imoved=slots[i].moved; + slots[i].sym->initialized=slots[i].initialized; + } +} + +static void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, + unsigned count) +{ + unsigned i; + for (i=0; imoved = left[i].moved==1 && right[i].moved==1 ? 1 : + (left[i].moved || right[i].moved ? 2 : 0); + base[i].sym->initialized = left[i].initialized && right[i].initialized; + } +} + static FeNode *find_const_node(FeCheck *c, const char *name) { FeNode *n; @@ -390,6 +472,7 @@ static FeType *check_struct_init(FeCheckerState *s, FeNode *n) } if (!field) { err(s->c,f->loc,"invalid enum payload field"); continue; } v=check_expr(s,f->a); + mark_moved(s,f->a,v); if (!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"enum payload type mismatch"); } } else if (n->children) err(s->c,n->loc,"empty enum variant cannot have payload"); @@ -402,6 +485,7 @@ static FeType *check_struct_init(FeCheckerState *s, FeNode *n) field=fe_type_field(t,f->text); if(!field) { err(s->c,f->loc,"invalid struct field"); continue; } v=check_expr(s,f->a); + mark_moved(s,f->a,v); if(!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"struct field type mismatch"); } for(i=0;ifield_count;i++) if(!has_field(n->children,t->fields[i].name)) err(s->c,n->loc,"missing struct field"); @@ -411,7 +495,7 @@ static FeType *check_struct_init(FeCheckerState *s, FeNode *n) static FeType *check_array_init(FeCheckerState *s, FeNode *n) { FeNode *x; FeType *elem=0; FeType *v; unsigned long count=0; - for(x=n->children;x;x=x->next) { v=check_expr(s,x); if(!elem) elem=v; else if(!compatible(elem,v,x)&&v->kind!=FE_TYPE_UNKNOWN) err(s->c,x->loc,"array element type mismatch"); ++count; } + for(x=n->children;x;x=x->next) { v=check_expr(s,x); mark_moved(s,x,v); if(!elem) elem=v; else if(!compatible(elem,v,x)&&v->kind!=FE_TYPE_UNKNOWN) err(s->c,x->loc,"array element type mismatch"); ++count; } if(!elem) elem=unknown(s->c); n->sem_type=fe_type_array(&s->c->types,count,elem); return n->sem_type; } @@ -446,6 +530,8 @@ static FeType *check_identifier(FeCheckerState *s, FeNode *n, int read) } n->cname = sym->cname; n->sem_type = sym->type; + if (sym->moved == 1) err(s->c,n->loc,"use of moved value"); + else if (sym->moved == 2) err(s->c,n->loc,"use of possibly moved value"); if (read && !sym->initialized && !sym->fn) err(s->c, n->loc, "use of uninitialized variable"); return sym->type; @@ -545,6 +631,34 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return a; } if (n->kind == FE_N_CALL) { + if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"drop")==0) { + err(c,n->loc,"drop may only be invoked by scope cleanup"); + return unknown(c); + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { + FeNode *arg=n->children; + if (strcmp(n->a->b->text,"destroy")==0) { + a=arg ? check_expr(s,arg) : unknown(c); + if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) + err(c,n->loc,"mem.destroy requires exactly one owned pointer"); + else + mark_moved(s,arg,a); + n->sem_type=fe_type_intern(&c->types,"void"); + return n->sem_type; + } + if (strcmp(n->a->b->text,"create")==0) { + if (!arg || arg->next || arg->kind!=FE_N_IDENT) + err(c,n->loc,"mem.create requires exactly one type argument"); + a=arg && arg->kind==FE_N_IDENT ? + fe_type_owned(&c->types,fe_type_intern(&c->types,arg->text)) : + fe_type_owned(&c->types,unknown(c)); + n->sem_type=fe_type_error_union(&c->types,a); + return n->sem_type; + } + } if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && @@ -606,6 +720,7 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) arg = n->children; while (param && arg) { a = check_expr(s, arg); + mark_moved(s,arg,a); b = node_type(c, param->a); if (!compatible(b, a, arg) && a->kind != FE_TYPE_UNKNOWN) err(c, arg->loc, "argument type mismatch"); @@ -634,6 +749,11 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) n->sem_type=a->elem; return a->elem; } + if (a->kind == FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=a->elem; + return a->elem; + } if(a->kind==FE_TYPE_STRUCT) { field=fe_type_field(a,n->b ? n->b->text : ""); if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } @@ -684,6 +804,11 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) n->sem_type=base->elem; return base->elem; } + if (base && base->kind == FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return base->elem; + } if (!lvalue_writable(s,n->a)) err(s->c,n->loc,"cannot assign through immutable value"); field=base && base->kind==FE_TYPE_STRUCT ? fe_type_field(base,n->b ? n->b->text : "") : 0; @@ -817,7 +942,7 @@ static void check_type_cycle(FeCheck *c, FeType *t) unsigned i; FeType *next; if (!t || t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR || - t->kind == FE_TYPE_REF || + t->kind == FE_TYPE_REF || t->kind == FE_TYPE_OWNED || t->kind == FE_TYPE_INT || t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR || t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) return; @@ -889,6 +1014,7 @@ static void check_stmt(FeCheckerState *s, FeNode *n) err(c, n->loc, "initializer type mismatch"); if (b->kind == FE_TYPE_VOID) err(c, n->loc, "void expression cannot initialize a variable"); + mark_moved(s,n->b,b); add_symbol(s, s->scope, n->text, a, 0, 0, 1, local_cname(c, n->text ? n->text : "local"), n); break; @@ -904,6 +1030,7 @@ static void check_stmt(FeCheckerState *s, FeNode *n) err(c, n->loc, "initializer type mismatch"); if (b->kind == FE_TYPE_VOID) err(c, n->loc, "void expression cannot initialize a variable"); + mark_moved(s,n->b,b); initialized = n->b != 0; add_symbol(s, s->scope, n->text, a, 0, 1, initialized, local_cname(c, n->text ? n->text : "local"), n); @@ -913,6 +1040,7 @@ static void check_stmt(FeCheckerState *s, FeNode *n) a = check_lvalue(s, n->a, compound_operator(n->text)); if (!compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) err(c, n->loc, "assignment type mismatch"); + mark_moved(s,n->b,b); sym = n->a && n->a->kind == FE_N_IDENT ? find_symbol(s->scope, n->a->text) : 0; if (sym && sym->mutable) sym->initialized = 1; @@ -924,27 +1052,67 @@ static void check_stmt(FeCheckerState *s, FeNode *n) (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION)) err(c,n->loc,"try requires an enclosing error result"); break; - case FE_N_IF: + case FE_N_DEFER: + ++s->defer_depth; + check_stmt(s,n->a); + --s->defer_depth; + break; + case FE_N_IF: { + FeFlowSlot base[128], left[128], right[128]; + unsigned flow_count; a = check_expr(s, n->a); if (known(a) && a->kind != FE_TYPE_BOOL) err(c, n->loc, "if condition must be bool"); + flow_count=flow_capture(s->scope,base,128); check_stmt(s, n->b); - check_stmt(s, n->c); + flow_capture(s->scope,left,flow_count); + flow_restore(base,flow_count); + if (n->c) check_stmt(s, n->c); + if (n->c) flow_capture(s->scope,right,flow_count); + else { + unsigned i; + for (i=0;ia); if (known(a) && a->kind != FE_TYPE_BOOL) err(c, n->loc, "while condition must be bool"); + flow_count=flow_capture(s->scope,base,128); + if (s->loop_depth < 255U) ++s->loop_depth; check_stmt(s, n->b); + if (s->loop_depth) --s->loop_depth; + flow_capture(s->scope,body,flow_count); + flow_restore(base,flow_count); + { + unsigned i; + for (i=0;iloop_depth < 255U) ++s->loop_depth; check_for(s,n); + if (s->loop_depth) --s->loop_depth; break; case FE_N_MATCH: check_match(s,n); break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (!s->loop_depth) err(c,n->loc,"break or continue outside loop"); + break; case FE_N_RETURN: b = n->a ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); + mark_moved(s,n->a,b); if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) err(c, n->loc, "void expression returned from value function"); else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && @@ -970,6 +1138,8 @@ static void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) s.globals = globals; s.scope = scope_new(&s, globals); s.ret = fn->b ? node_type(c, fn->b) : fe_type_intern(&c->types, "void"); + s.loop_depth=0; + s.defer_depth=0; fn->sem_type = s.ret; for (x = fn->a ? fn->a->children : 0; x; x = x->next) { t = node_type(c, x->a); @@ -1047,5 +1217,7 @@ FeType *fe_check_expr_type(FeCheck *c, FeNode *n) s.scope = scope_new(&s, 0); s.globals = s.scope; s.ret = fe_type_intern(&c->types, "void"); + s.loop_depth=0; + s.defer_depth=0; return check_expr(&s, n); } diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index ff75623..ecba29f 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -2,6 +2,10 @@ #include #include +static void emit_expr(FeEmitter *e, FeNode *n); +static void emit_stmt(FeEmitter *e, FeNode *n); +static void emit_block(FeEmitter *e, FeNode *n); + static void pad(FeEmitter *e) { int i; @@ -24,6 +28,19 @@ static const char *cname(FeNode *n, const char *fallback) static void emit_one_type(FeEmitter *e, FeType *t); +static int type_needs_drop(FeType *t) +{ + unsigned i; + if (!t) return 0; + if (t->kind==FE_TYPE_OWNED) return 1; + if (t->kind==FE_TYPE_ARRAY) return type_needs_drop(t->elem); + if (t->kind==FE_TYPE_STRUCT) { + if (t->has_drop) return 1; + for (i=0;ifield_count;i++) if (type_needs_drop(t->fields[i].type)) return 1; + } + return 0; +} + static void emit_type_deps(FeEmitter *e, FeType *t) { unsigned i,j; @@ -35,6 +52,8 @@ static void emit_type_deps(FeEmitter *e, FeType *t) for (i=0;ivariant_count;i++) for (j=0;jvariants[i].field_count;j++) emit_one_type(e,t->variants[i].fields[j].type); + if (t->kind == FE_TYPE_ERROR_UNION && t->error_value) + emit_one_type(e,t->error_value); } static void emit_one_type(FeEmitter *e, FeType *t) @@ -43,7 +62,10 @@ static void emit_one_type(FeEmitter *e, FeType *t) if (t && strcmp(t->name,"io.Writer")==0) return; if (!t || t->emit_state || (t->kind != FE_TYPE_STRUCT && t->kind != FE_TYPE_ENUM && - t->kind != FE_TYPE_ARRAY && t->kind != FE_TYPE_SLICE)) return; + t->kind != FE_TYPE_ARRAY && t->kind != FE_TYPE_SLICE && + t->kind != FE_TYPE_ERROR_UNION) || + (t->kind == FE_TYPE_ERROR_UNION && + (!t->error_value || t->error_value->kind == FE_TYPE_VOID))) return; t->emit_state=1; emit_type_deps(e,t); if(t->kind==FE_TYPE_STRUCT) { @@ -55,6 +77,10 @@ static void emit_one_type(FeEmitter *e, FeType *t) } else if(t->kind==FE_TYPE_SLICE && t->cname) { fputs("typedef struct { ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); fputs(";\n",e->out); fprintf(e->out,"static %s %s(%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n",t->cname,t->maker,fe_type_c_name(t->elem,e->pointer_bits),t->cname); + } else if(t->kind==FE_TYPE_ERROR_UNION) { + fputs(t->cname,e->out); fputs(" { unsigned short e; ",e->out); + fputs(fe_type_c_name(t->error_value,e->pointer_bits),e->out); + fputs(" v; } ;\n",e->out); } else if(t->kind==FE_TYPE_ENUM) { for(i=0;ivariant_count;i++) if(t->variants[i].field_count>1) { fprintf(e->out,"struct fe_payload_%s_%s {",t->name,t->variants[i].name); @@ -81,10 +107,95 @@ static void emit_type_defs(FeEmitter *e) for(t=e->check->types.types;t;t=t->next) emit_one_type(e,t); } +static FeNode *find_drop_method(FeEmitter *e, const char *name) +{ + FeNode *n; + FeNode *m; + for (n=e->check->ast->root ? e->check->ast->root->children : 0; n; n=n->next) + if (n->kind==FE_N_STRUCT && n->text && name && strcmp(n->text,name)==0) + for (m=n->children; m; m=m->next) + if (m->kind==FE_N_FN && m->text && strcmp(m->text,"drop")==0) + return m; + return 0; +} + +static void emit_drop_fields(FeEmitter *e, FeType *t) +{ + unsigned i; + FeType *ft; + for (i=t->field_count; i>0; --i) { + ft=t->fields[i-1].type; + if (!type_needs_drop(ft)) continue; + if (ft->kind==FE_TYPE_OWNED) { + fputs("if (self->",e->out); fputs(t->fields[i-1].name,e->out); + fputs(") { ",e->out); + if (ft->elem && type_needs_drop(ft->elem) && ft->elem->drop_cname) { + fprintf(e->out,"%s(self->%s); ",ft->elem->drop_cname,t->fields[i-1].name); + } + fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); + fputs("); self->",e->out); fputs(t->fields[i-1].name,e->out); + fputs("=0; }\n",e->out); + } else if (ft->kind==FE_TYPE_STRUCT && ft->drop_cname) { + fprintf(e->out,"%s(&self->%s);\n",ft->drop_cname,t->fields[i-1].name); + } else if (ft->kind==FE_TYPE_ARRAY && ft->drop_cname) { + fprintf(e->out,"%s(&self->%s);\n",ft->drop_cname,t->fields[i-1].name); + } + } +} + +static void emit_drop_helpers(FeEmitter *e) +{ + FeType *t; + FeNode *method; + FeNode *param; + for (t=e->check->types.types; t; t=t->next) + if ((t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY) && + type_needs_drop(t) && t->drop_cname) + fprintf(e->out,"static void %s(%s *self);\n",t->drop_cname,t->cname); + for (t=e->check->types.types; t; t=t->next) { + if (t->kind!=FE_TYPE_STRUCT || !type_needs_drop(t) || !t->drop_cname) continue; + fprintf(e->out,"static void %s(%s *self) {\n",t->drop_cname,t->cname); + method=find_drop_method(e,t->name); + if (method) { + param=method->a ? method->a->children : 0; + if (param) param->cname="self"; + if (method->c) emit_block(e,method->c); + fputc('\n',e->out); + } + emit_drop_fields(e,t); + fputs("}\n",e->out); + } + for (t=e->check->types.types; t; t=t->next) + if (t->kind==FE_TYPE_ARRAY && type_needs_drop(t) && t->drop_cname) { + fprintf(e->out,"static void %s(%s *self) { unsigned long i; for (i=0; i<%lu; ++i) { ", + t->drop_cname,t->cname,t->length); + if (t->elem->kind==FE_TYPE_OWNED) { + fputs("if (self->a[i]) { ",e->out); + if (t->elem->elem && type_needs_drop(t->elem->elem) && t->elem->elem->drop_cname) + fprintf(e->out,"%s(self->a[i]); ",t->elem->elem->drop_cname); + fputs("free(self->a[i]); self->a[i]=0; }",e->out); + } else if (t->elem->drop_cname) + fprintf(e->out,"%s(&self->a[i]);",t->elem->drop_cname); + fputs(" } }\n",e->out); + } +} + static void emit_type_helpers(FeEmitter *e) { FeType *t; unsigned i,j; + for(t=e->check->types.types;t;t=t->next) { + if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && + t->error_value->kind!=FE_TYPE_VOID) { + fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", + t->cname,t->maker,fe_type_c_name(t->error_value,e->pointer_bits),t->cname); + if (t->error_value->kind==FE_TYPE_OWNED) + fprintf(e->out,"static %s %s(void) { %s r; r.v=(%s)malloc(sizeof(%s)); r.e=r.v ? 0 : 1; return r; }\n", + t->cname,t->alloc_cname,t->cname, + fe_type_c_name(t->error_value,e->pointer_bits), + fe_type_c_name(t->error_value->elem,e->pointer_bits)); + } + } for(t=e->check->types.types;t;t=t->next) { if(t->kind==FE_TYPE_STRUCT && t->maker) { fprintf(e->out,"static %s %s(",t->cname,t->maker); @@ -109,6 +220,7 @@ static void emit_type_helpers(FeEmitter *e) } } } + emit_drop_helpers(e); for(t=e->check->types.types;t;t=t->next) { if (t->kind==FE_TYPE_ARRAY && t->indexer) { fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", @@ -501,6 +613,9 @@ static void emit_lvalue(FeEmitter *e, FeNode *n) if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && n->b && n->b->text && strcmp(n->b->text,"^")==0) { fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); + } else if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OWNED && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); } else { emit_lvalue(e,n->a); fputc('.',e->out); fputs(n->b ? n->b->text : "member",e->out); } return; } @@ -512,6 +627,19 @@ static void emit_lvalue(FeEmitter *e, FeNode *n) emit_expr(e,n); } +static void emit_destroy_expr(FeEmitter *e, FeNode *n) +{ + fputs("(free(",e->out); emit_expr(e,n); fputs(")",e->out); + if (n && n->kind==FE_N_IDENT) { + fputs(", ",e->out); emit_lvalue(e,n); + fputs("=0, fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("=0",e->out); + } else { + fputs(", ",e->out); emit_lvalue(e,n); fputs("=0",e->out); + } + fputs(", 0)",e->out); +} + static FeNode *init_field(FeNode *n, const char *name) { FeNode *f; @@ -609,7 +737,15 @@ static void emit_expr(FeEmitter *e, FeNode *n) } case FE_N_UNARY: op = n->text ? n->text : ""; - if (strcmp(op, "try") == 0) { emit_expr(e,n->a); break; } + if (strcmp(op, "try") == 0) { + if (n->a && n->a->sem_type && + n->a->sem_type->kind==FE_TYPE_ERROR_UNION && + n->a->sem_type->error_value && + n->a->sem_type->error_value->kind!=FE_TYPE_VOID) { + fputc('(',e->out); emit_expr(e,n->a); fputs(").v",e->out); + } else emit_expr(e,n->a); + break; + } if (strcmp(op, "not") == 0) fputs("(!", e->out); else { fputc('(', e->out); @@ -641,6 +777,27 @@ static void emit_expr(FeEmitter *e, FeNode *n) FeVariantType *v; int special=0; if(n->text && (strcmp(n->text,"@print")==0 || strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { emit_m4_builtin(e,n); special=1; } + else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && + n->a->b->text && strcmp(n->a->b->text,"destroy")==0 && + n->children) { + emit_destroy_expr(e,n->children); + special=1; + } + else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && + n->a->b->text && strcmp(n->a->b->text,"create")==0 && + n->children && n->children->kind==FE_N_IDENT) { + FeType *created=fe_type_intern(&e->check->types,n->children->text); + FeType *owned=fe_type_owned(&e->check->types,created); + FeType *result=fe_type_error_union(&e->check->types,owned); + if (result->alloc_cname) fputs(result->alloc_cname,e->out); + else fputs("fe_bad_alloc",e->out); + fputs("()",e->out); + special=1; + } else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && @@ -660,7 +817,11 @@ static void emit_expr(FeEmitter *e, FeNode *n) fputc('(', e->out); for (x = n->children; x; x = x->next) { if (x != n->children) fputs(", ", e->out); - emit_expr(e, x); + if ((x->flags & 0x100U) && x->kind==FE_N_IDENT && + x->sem_type && x->sem_type->kind==FE_TYPE_OWNED) { + fputs("(fe_live_",e->out); fputs(cname(x,"owned"),e->out); + fputs("=0, ",e->out); emit_expr(e,x); fputc(')',e->out); + } else emit_expr(e, x); } fputc(')', e->out); } @@ -674,6 +835,9 @@ static void emit_expr(FeEmitter *e, FeNode *n) else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && n->b && n->b->text && strcmp(n->b->text,"^")==0) { fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); + } else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OWNED && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); } else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_ENUM) { v=fe_type_variant(n->a->sem_type,n->b ? n->b->text : ""); if(v) fputs(v->maker,e->out); else fputs("0",e->out); if(v)fputs("()",e->out); } else { emit_expr(e, n->a); fputc('.', e->out); if (n->b) fputs(n->b->text ? n->b->text : "member", e->out); } break; @@ -701,6 +865,89 @@ static void emit_decl(FeEmitter *e, FeNode *n) } else emit_expr(e,n->b); } fputs(";\n", e->out); + if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && + n->sem_type->kind==FE_TYPE_OWNED) { + pad(e); fputs("unsigned char fe_live_",e->out); + fputs(cname(n,"owned"),e->out); fputs("=0;\n",e->out); + } +} + +static void emit_owned_live(FeEmitter *e, FeNode *n, int value) +{ + if (n && n->sem_type && n->sem_type->kind==FE_TYPE_OWNED) { + pad(e); fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fprintf(e->out,"=%d;\n",value); + } +} + +static void emit_value_drop(FeEmitter *e, FeNode *n) +{ + FeType *t=n ? n->sem_type : 0; + if (!n || !t || !type_needs_drop(t) || (n->flags & 0x100U) || + (n->flags & 0x200U)) return; + if (t->kind==FE_TYPE_OWNED) { + pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs(") { ",e->out); + if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) { + fprintf(e->out,"%s(%s); ",t->elem->drop_cname,cname(n,"owned")); + } + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); + fputs("); fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("=0; }\n",e->out); + } else if (t->drop_cname) { + pad(e); fprintf(e->out,"%s(&%s);\n",t->drop_cname,cname(n,"local")); + } +} + +static void emit_cleanup_block(FeEmitter *e, FeNode *n) +{ + FeNode *x; + unsigned count=0; + unsigned index; + unsigned seen=0xffffffffU; + unsigned depth; + /* A defer becomes active only after its declaration statement was + reached. In particular, a failing initializer must not run a later + defer merely because it shares this AST block. */ + for (depth=0; depthblock_depth; ++depth) + if (e->block_stack[depth]==n) { seen=e->block_seen[depth]; break; } + for (x=n ? n->children : 0, index=0; x; x=x->next, ++index) + if (indexkind==FE_N_DEFER || x->kind==FE_N_LET || x->kind==FE_N_VAR)) ++count; + while (count) { + index=0; + for (x=n->children; x; x=x->next) + if ((x->kind==FE_N_DEFER || x->kind==FE_N_LET || x->kind==FE_N_VAR) && + index++==count-1) { + if (x->kind==FE_N_DEFER) emit_stmt(e,x->a); else emit_value_drop(e,x); + break; + } + --count; + } +} + +static void emit_cleanup_to(FeEmitter *e, unsigned floor) +{ + unsigned i; + for (i=e->block_depth; i>floor; --i) emit_cleanup_block(e,e->block_stack[i-1]); +} + +static void emit_cleanup_all(FeEmitter *e) +{ + emit_cleanup_to(e,0); +} + +static void emit_error_return(FeEmitter *e, const char *error_expr) +{ + if (e->current_ret && e->current_ret->kind==FE_TYPE_ERROR_UNION && + e->current_ret->error_value && e->current_ret->error_value->kind!=FE_TYPE_VOID) { + fputs("return ",e->out); fputs(e->current_ret->maker,e->out); + fputs("(",e->out); fputs(error_expr,e->out); fputs(", (",e->out); + fputs(fe_type_c_name(e->current_ret->error_value,e->pointer_bits),e->out); + fputs(")0);\n",e->out); + } else { + fputs("return ",e->out); fputs(error_expr,e->out); fputs(";\n",e->out); + } } static void emit_block(FeEmitter *e, FeNode *n) @@ -718,12 +965,30 @@ static void emit_block(FeEmitter *e, FeNode *n) pad(e); fputs("{\n", e->out); ++e->indent; + if (e->block_depth<32U) { + e->block_stack[e->block_depth]=n; + e->block_seen[e->block_depth]=0; + ++e->block_depth; + } /* C89 requires declarations before statements in each actual block. */ for (x = n->children; x; x = x->next) if (x->kind == FE_N_LET || x->kind == FE_N_VAR || x->kind == FE_N_CONST) emit_decl(e, x); - for (x = n->children; x; x = x->next) emit_stmt(e, x); + if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs(fe_type_c_name(e->current_ret,e->pointer_bits),e->out); + fputs(" fe_return_value;\n",e->out); + } + { + unsigned seen=0; + for (x = n->children; x; x = x->next) { + ++seen; + if (e->block_depth) e->block_seen[e->block_depth-1]=seen; + emit_stmt(e, x); + } + } --e->indent; + emit_cleanup_block(e,n); + if (e->block_depth) --e->block_depth; if (e->fallthrough_block==n) { pad(e); fputs("return 0;\n",e->out); @@ -765,6 +1030,37 @@ static void emit_match(FeEmitter *e, FeNode *n, int value_context) } } +static int try_has_value(FeNode *n) +{ + return n && n->kind==FE_N_UNARY && n->text && strcmp(n->text,"try")==0 && + n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_ERROR_UNION && + n->a->sem_type->error_value && n->a->sem_type->error_value->kind!=FE_TYPE_VOID; +} + +static void emit_try_statement(FeEmitter *e, FeNode *try_node, FeNode *target) +{ + char temp[40]; + char error[48]; + FeType *result=try_node->a->sem_type; + sprintf(temp,"fe_try_%u",e->temp_serial++); + sprintf(error,"%s.e",temp); + pad(e); fputs("{ ",e->out); fputs(fe_type_c_name(result,e->pointer_bits),e->out); + fputc(' ',e->out); fputs(temp,e->out); fputs(" = ",e->out); + emit_expr(e,try_node->a); fputs("; if (",e->out); fputs(temp,e->out); + fputs(".e) {\n",e->out); ++e->indent; + emit_cleanup_all(e); + pad(e); emit_error_return(e,error); + --e->indent; pad(e); fputs("} ",e->out); + if (target) { + if (target->kind==FE_N_LET || target->kind==FE_N_VAR) + fputs(cname(target,"fe_local"),e->out); + else + emit_lvalue(e,target); + fputs(" = ",e->out); fputs(temp,e->out); fputs(".v;\n",e->out); + } + fputs("}\n",e->out); +} + static void emit_stmt(FeEmitter *e, FeNode *n) { if (!n) return; @@ -776,46 +1072,91 @@ static void emit_stmt(FeEmitter *e, FeNode *n) case FE_N_LET: case FE_N_VAR: if (n->b) { - pad(e); - fputs(cname(n, "fe_local"), e->out); - fputs(" = ", e->out); - emit_expr(e, n->b); - fputs(";\n", e->out); + if (try_has_value(n->b)) emit_try_statement(e,n->b,n); + else { + pad(e); + fputs(cname(n, "fe_local"), e->out); + fputs(" = ", e->out); + emit_expr(e, n->b); + fputs(";\n", e->out); + } + emit_owned_live(e,n,1); } break; case FE_N_ASSIGN: + if (n->a && n->a->kind==FE_N_IDENT && n->a->sem_type && + n->a->sem_type->kind==FE_TYPE_OWNED) { + pad(e); fputs("if (fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); + fputs(") { ",e->out); + if (n->a->sem_type->elem && type_needs_drop(n->a->sem_type->elem) && + n->a->sem_type->elem->drop_cname) + fprintf(e->out,"%s(%s); ",n->a->sem_type->elem->drop_cname,cname(n->a,"owned")); + fputs("free(",e->out); fputs(cname(n->a,"owned"),e->out); + fputs("); fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); + fputs("=0; }\n",e->out); + } pad(e); emit_lvalue(e, n->a); fputc(' ', e->out); fputs(n->text ? n->text : "=", e->out); fputs(" ", e->out); - emit_expr(e, n->b); + if (n->b && (n->b->flags & 0x100U) && n->b->kind==FE_N_IDENT && + n->b->sem_type && n->b->sem_type->kind==FE_TYPE_OWNED) { + fputs("(fe_live_",e->out); fputs(cname(n->b,"owned"),e->out); + fputs("=0, ",e->out); emit_expr(e,n->b); fputc(')',e->out); + } else emit_expr(e, n->b); fputs(";\n", e->out); + if (n->a && n->a->kind==FE_N_IDENT) emit_owned_live(e,n->a,1); break; case FE_N_EXPR_STMT: - pad(e); - if (n->a && n->a->kind==FE_N_UNARY && n->a->text && - strcmp(n->a->text,"try")==0 && n->a->a) { - fputs("if ((fe_m4_error = ",e->out); - emit_expr(e,n->a->a); - fputs(") != 0) return fe_m4_error;\n",e->out); + if (try_has_value(n->a)) { + emit_try_statement(e,n->a,0); } else { - emit_expr(e, n->a); - fputs(";\n", e->out); + pad(e); + if (n->a && n->a->kind==FE_N_UNARY && n->a->text && + strcmp(n->a->text,"try")==0 && n->a->a) { + fputs("if ((fe_m4_error = ",e->out); + emit_expr(e,n->a->a); + fputs(") != 0) {\n",e->out); + ++e->indent; + emit_cleanup_all(e); + pad(e); fputs("return fe_m4_error;\n",e->out); + --e->indent; + pad(e); fputs("}\n",e->out); + } else { + emit_expr(e, n->a); + fputs(";\n", e->out); + } + } + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (e->loop_depth) { + emit_cleanup_to(e,e->loop_floor[e->loop_depth-1]); + pad(e); fputs(n->kind==FE_N_BREAK ? "break;\n" : "continue;\n",e->out); } break; case FE_N_RETURN: - pad(e); if (n->a && n->a->kind == FE_N_MATCH) { emit_match(e,n->a,1); break; } - fputs("return", e->out); - if (n->a) { - fputc(' ', e->out); - emit_expr(e, n->a); + /* Evaluate before cleanup: `return p.^` must not dereference p after + its owned cleanup has run. The block-local temporary is declared + before all statements to retain C89 declaration ordering. */ + if (n->a && e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs("fe_return_value = ",e->out); + if ((n->a->flags & 0x100U) && n->a->kind==FE_N_IDENT && + n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OWNED) { + fputs("(fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); + fputs("=0, ",e->out); emit_expr(e,n->a); fputc(')',e->out); + } else emit_expr(e,n->a); + fputs(";\n",e->out); } - fputs(";\n", e->out); + emit_cleanup_all(e); + pad(e); fputs("return",e->out); + if (n->a) fputs(" fe_return_value",e->out); + fputs(";\n",e->out); break; case FE_N_IF: pad(e); @@ -836,12 +1177,15 @@ static void emit_stmt(FeEmitter *e, FeNode *n) fputs("while (", e->out); emit_expr(e, n->a); fputs(") ", e->out); + if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; if (n->b && n->b->kind == FE_N_BLOCK) emit_block(e, n->b); else emit_block(e, 0); + if (e->loop_depth) --e->loop_depth; fputc('\n', e->out); break; case FE_N_FOR: pad(e); fputs("{\n",e->out); ++e->indent; + if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; if (n->c) { pad(e); fputs("unsigned long ",e->out); fputs(cname(n,"fe_index"),e->out); fputs(";\n",e->out); pad(e); fputs(cname(n,"fe_index"),e->out); fputs(" = ",e->out); emit_expr(e,n->a); fputs(";\n",e->out); @@ -869,7 +1213,9 @@ static void emit_stmt(FeEmitter *e, FeNode *n) fputc('&',e->out); if(mutable_iter) fputs("(*",e->out); fputs(temp,e->out); if(mutable_iter) fputs(")",e->out); if(bt && bt->kind==FE_TYPE_ARRAY) fputs(".a[fe_i]",e->out); else fputs(".p[fe_i]",e->out); fputs("; ",e->out); emit_block(e,n->b); fputs(" } }\n",e->out); } - --e->indent; pad(e); fputs("}\n",e->out); break; + --e->indent; + if (e->loop_depth) --e->loop_depth; + pad(e); fputs("}\n",e->out); break; case FE_N_MATCH: emit_match(e,n,0); break; default: @@ -899,12 +1245,15 @@ static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) fputc(')', e->out); if (prototype) fputs(";\n", e->out); else { + FeType *old_ret=e->current_ret; + e->current_ret=fn->sem_type; fputs(" ", e->out); if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && fn->sem_type->error_value && fn->sem_type->error_value->kind==FE_TYPE_VOID) e->fallthrough_block=fn->c; emit_block(e, fn->c); + e->current_ret=old_ret; fputc('\n', e->out); } } @@ -933,6 +1282,9 @@ void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, e->no_checks = no_checks; e->temp_serial = 0; e->fallthrough_block = 0; + e->block_depth = 0; + e->loop_depth = 0; + e->current_ret = 0; } void fe_emit_c_program(FeEmitter *e) diff --git a/fec/src/emit_c.h b/fec/src/emit_c.h index bc927a5..93a9bca 100644 --- a/fec/src/emit_c.h +++ b/fec/src/emit_c.h @@ -11,6 +11,12 @@ typedef struct FeEmitter { int no_checks; unsigned temp_serial; FeNode *fallthrough_block; + FeNode *block_stack[32]; + unsigned block_seen[32]; + unsigned block_depth; + unsigned loop_floor[16]; + unsigned loop_depth; + FeType *current_ret; } FeEmitter; void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, diff --git a/fec/src/types.c b/fec/src/types.c index 3d77341..f039b6d 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -19,10 +19,13 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->slicer = 0; t->full_slicer = 0; t->tail_slicer = 0; + t->drop_cname = 0; + t->alloc_cname = 0; t->bits = 0; t->is_unsigned = 0; t->packed = 0; t->is_error = 0; + t->has_drop = 0; t->length = 0; t->size = 0; t->align = 1; @@ -125,6 +128,7 @@ FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem) t->elem = elem; t->cname = generated_name(ctx, "struct fe_arr_", "type"); t->maker = generated_name(ctx, "fe_make_arr_", "type"); + t->drop_cname = generated_name(ctx, "fe_drop_arr_", "type"); t->indexer = generated_name(ctx, "fe_idx_arr_", "type"); t->slicer = generated_name(ctx, "fe_slice_arr_", "type"); t->full_slicer = generated_name(ctx, "fe_full_arr_", "type"); @@ -166,6 +170,19 @@ FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) return t; } +FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem) +{ + char key[128]; + FeType *t; + sprintf(key,"^%s",elem ? elem->name : "?"); + t=fe_type_intern(ctx,key); + if(t->kind==FE_TYPE_UNKNOWN) { + t->kind=FE_TYPE_OWNED; + t->elem=elem; + } + return t; +} + FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value) { char key[128]; @@ -175,6 +192,11 @@ FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value) if(t->kind==FE_TYPE_UNKNOWN) { t->kind=FE_TYPE_ERROR_UNION; t->error_value=value; + if (value && value->kind != FE_TYPE_VOID) { + t->cname=generated_name(ctx,"struct fe_result_","value"); + t->maker=generated_name(ctx,"fe_make_result_","value"); + t->alloc_cname=generated_name(ctx,"fe_alloc_result_","value"); + } } return t; } @@ -192,6 +214,9 @@ FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) if (t->kind == FE_TYPE_STRUCT) return t; t->kind = FE_TYPE_STRUCT; t->packed = packed; + for (f = node->children; f; f = f->next) + if (f->kind==FE_N_FN && f->text && strcmp(f->text,"drop")==0) + t->has_drop=1; cname = (char *)fe_arena_alloc(ctx->arena, (unsigned long)strlen("struct fe_") + strlen(ctx->unit_name) + strlen(node->text) + 2UL); @@ -202,6 +227,7 @@ FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) strcat(cname, node->text); t->cname = cname; t->maker = generated_name(ctx, "fe_make_", node->text); + t->drop_cname = generated_name(ctx, "fe_drop_", node->text); for (f = node->children; f; f = f->next) if (f->kind == FE_N_FIELD) ++count; t->field_count = count; @@ -337,7 +363,15 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) if (t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->cycle_state = 2; return; } if (t->kind == FE_TYPE_ERROR_UNION) { - t->size = 2; t->align = ctx->pointer_bits == 16 ? 1U : 2U; + if (t->error_value && t->error_value->kind != FE_TYPE_VOID) { + layout_type(ctx,t->error_value); + t->align=ctx->pointer_bits==16 ? 1U : fe_type_align(t->error_value); + t->size=round_up(2UL,t->align)+fe_type_size(t->error_value); + t->size=round_up(t->size,t->align); + } else { + t->size=2; + t->align=ctx->pointer_bits==16 ? 1U : 2U; + } t->cycle_state = 2; return; } if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) { @@ -354,6 +388,11 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) t->align = ctx->pointer_bits == 16 ? 1U : 4U; t->cycle_state = 2; return; } + if (t->kind == FE_TYPE_OWNED) { + t->size = ctx->pointer_bits == 16 ? 2UL : 4UL; + t->align = ctx->pointer_bits == 16 ? 1U : 4U; + t->cycle_state = 2; return; + } if (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR) { t->size = ctx->pointer_bits == 16 ? 4UL : 8UL; t->align = ctx->pointer_bits == 16 ? 1U : 4U; @@ -449,6 +488,8 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) strcmp(node->text, "&mut") == 0)) return fe_type_ref(ctx, fe_type_from_ast(ctx,node->a), strcmp(node->text,"&mut") == 0); + if (node->text && strcmp(node->text,"^")==0) + return fe_type_owned(ctx,fe_type_from_ast(ctx,node->a)); if (node->text && strcmp(node->text, "[") == 0) { if (node->a) { if (node->a->kind == FE_N_LITERAL && node->a->text) @@ -495,7 +536,11 @@ const char *fe_type_c_name(const FeType *t, unsigned pointer_bits) if (!t) return "long"; if (t->cname) return t->cname; if (t->kind == FE_TYPE_VOID) return "void"; - if (t->kind == FE_TYPE_ERROR_UNION) return "unsigned short"; + if (t->kind == FE_TYPE_ERROR_UNION) { + if (t->error_value && t->error_value->kind != FE_TYPE_VOID && t->cname) + return t->cname; + return "unsigned short"; + } if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) return "unsigned char"; if (t->kind == FE_TYPE_REF) { static char ref_name[128]; @@ -509,6 +554,12 @@ const char *fe_type_c_name(const FeType *t, unsigned pointer_bits) } return ref_name; } + if (t->kind == FE_TYPE_OWNED) { + static char owned_name[128]; + strcpy(owned_name,fe_type_c_name(t->elem,pointer_bits)); + strcat(owned_name," *"); + return owned_name; + } if (t->kind != FE_TYPE_INT) return "long"; if (strcmp(t->name, "usize") == 0) return pointer_bits == 16 ? "unsigned short" : "unsigned long"; if (strcmp(t->name, "isize") == 0) return pointer_bits == 16 ? "short" : "long"; diff --git a/fec/src/types.h b/fec/src/types.h index 16fa67e..4acab3c 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -6,7 +6,7 @@ typedef enum FeTypeKind { FE_TYPE_ERROR, FE_TYPE_ERROR_UNION, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, FE_TYPE_STRUCT, FE_TYPE_ENUM, FE_TYPE_ARRAY, FE_TYPE_SLICE, FE_TYPE_STR, - FE_TYPE_REF, FE_TYPE_UNKNOWN + FE_TYPE_REF, FE_TYPE_OWNED, FE_TYPE_UNKNOWN } FeTypeKind; typedef struct FeFieldType FeFieldType; @@ -37,10 +37,13 @@ struct FeType { char *slicer; char *full_slicer; char *tail_slicer; + char *drop_cname; + char *alloc_cname; unsigned bits; int is_unsigned; int packed; int is_error; + int has_drop; unsigned long length; unsigned long size; unsigned align; @@ -71,6 +74,7 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node); FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem); FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable); +FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value); FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed); FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node); diff --git a/fec/test-dos.bat b/fec/test-dos.bat index acc08bb..5418842 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -227,6 +227,25 @@ fec.exe --target=bits32 --emit-c TESTS\M4\BAD-OPEN.FE -o TESTS\M4\BAD-OPEN.C > n if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-CLS.FE -o TESTS\M4\BAD-CLS.C > nul if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\DEFER.FE -o TESTS\M5\DEFER.C > nul +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\OWNED.FE -o TESTS\M5\OWNED.C > nul +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-MOVE.FE -o TESTS\M5\BAD-MOVE.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DES.FE -o TESTS\M5\BAD-DES.C > nul +if not errorlevel 1 goto test_fail +if exist TESTS\M5\RUNTIME-G.C del TESTS\M5\RUNTIME-G.C +if exist TESTS\M5\RUNTIME.O del TESTS\M5\RUNTIME.O +if exist TESTS\M5\RUNTIME.EXE del TESTS\M5\RUNTIME.EXE +fec.exe --target=bits32 --emit-c TESTS\M5\RUNTIME.FE -o TESTS\M5\RUNTIME-G.C > nul +if errorlevel 1 goto test_fail +rem Compile generated source and the C89 runtime harness in one WCL386 invocation +rem so both objects use the same DOS/4GW startup and runtime library. +wcl386 -q -za -bt=dos -fe=TESTS\M5\RUNTIME.EXE TESTS\M5\RUNTIME-G.C TESTS\M5\RUNTIME.C +if errorlevel 1 goto test_fail +TESTS\M5\RUNTIME.EXE +if errorlevel 1 goto test_fail echo OK>TEST.OK cd C:\FEC diff --git a/fec/tests/m5/bad-destroy.fe b/fec/tests/m5/bad-destroy.fe new file mode 100644 index 0000000..bc60537 --- /dev/null +++ b/fec/tests/m5/bad-destroy.fe @@ -0,0 +1,5 @@ +unit m5_bad_destroy; + +fn bad(x: i32) -> void { + mem.destroy(x); +} diff --git a/fec/tests/m5/bad-move.fe b/fec/tests/m5/bad-move.fe new file mode 100644 index 0000000..6614cff --- /dev/null +++ b/fec/tests/m5/bad-move.fe @@ -0,0 +1,8 @@ +unit m5_bad_move; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn twice(p: ^i32) -> void { + take(p); + take(p); +} diff --git a/fec/tests/m5/defer.fe b/fec/tests/m5/defer.fe new file mode 100644 index 0000000..b21cbe3 --- /dev/null +++ b/fec/tests/m5/defer.fe @@ -0,0 +1,5 @@ +unit m5_defer; + +pub fn cleanup(p: ^i32) -> void { + defer { mem.destroy(p); } +} diff --git a/fec/tests/m5/owned.fe b/fec/tests/m5/owned.fe new file mode 100644 index 0000000..7f7a553 --- /dev/null +++ b/fec/tests/m5/owned.fe @@ -0,0 +1,9 @@ +unit m5_owned; + +fn main() -> void { + var p: ^i32 = try mem.create(i32); + p = try mem.create(i32); + p.^ = 7; + let value: i32 = p.^; + defer { mem.destroy(p); } +} diff --git a/fec/tests/m5/runtime.c b/fec/tests/m5/runtime.c new file mode 100644 index 0000000..2f3a110 --- /dev/null +++ b/fec/tests/m5/runtime.c @@ -0,0 +1,9 @@ +extern long fe_m5_runtime_run(long mode); + +int main(void) +{ + if (fe_m5_runtime_run(0) != 0) return 1; + if (fe_m5_runtime_run(1) != 9) return 2; + if (fe_m5_runtime_run(2) != 0) return 3; + return 0; +} diff --git a/fec/tests/m5/runtime.fe b/fec/tests/m5/runtime.fe new file mode 100644 index 0000000..be03a9d --- /dev/null +++ b/fec/tests/m5/runtime.fe @@ -0,0 +1,19 @@ +unit m5_runtime; + +pub fn run(mode: i32) -> i32 { + var p: ^i32 = try mem.create(i32); + defer { mem.destroy(p); } + p.^ = 7; + if mode == 1 { + p = try mem.create(i32); + p.^ = 9; + return p.^; + } + while true { + break; + } + if mode == 2 { + return 0; + } + return p.^ - 7; +} diff --git a/fec/tests/run-tests.sh b/fec/tests/run-tests.sh index 5d211ac..9f159a7 100644 --- a/fec/tests/run-tests.sh +++ b/fec/tests/run-tests.sh @@ -91,3 +91,14 @@ for f in bad-many bad-open bad-cls; do fi done echo "M4 tests: formatting builtins passed" + +for f in defer owned; do + "$root"/fec --target=bits32 --emit-c "$root"/tests/m5/$f.fe -o "$m4tmp/m5-$f.c" +done +for f in bad-move bad-destroy; do + if "$root"/fec --target=bits32 --emit-c "$root"/tests/m5/$f.fe -o "$m4tmp/m5-$f.c" >/dev/null 2>/dev/null; then + echo "FAIL (accepted M5 ownership error): $f.fe" + exit 1 + fi +done +echo "M5 tests: owned move/defer checks passed" diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat index b5755f1..e6e76a3 100644 --- a/fec/vm-m1.bat +++ b/fec/vm-m1.bat @@ -9,6 +9,7 @@ if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL if not exist C:\FEC\TESTS\M2 md C:\FEC\TESTS\M2 if not exist C:\FEC\TESTS\M3 md C:\FEC\TESTS\M3 if not exist C:\FEC\TESTS\M4 md C:\FEC\TESTS\M4 +if not exist C:\FEC\TESTS\M5 md C:\FEC\TESTS\M5 if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL if exist C:\FEC\STAGE.FAIL del C:\FEC\STAGE.FAIL @@ -169,6 +170,18 @@ copy D:\FEC\TESTS\M4\BAD-OPEN.FE C:\FEC\TESTS\M4\BAD-OPEN.FE > nul if errorlevel 1 goto stage_fail copy D:\FEC\TESTS\M4\BAD-CLS.FE C:\FEC\TESTS\M4\BAD-CLS.FE > nul if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M5\DEFER.FE C:\FEC\TESTS\M5\DEFER.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M5\OWNED.FE C:\FEC\TESTS\M5\OWNED.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M5\BAD-MOVE.FE C:\FEC\TESTS\M5\BAD-MOVE.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M5\BAD-DES~1.FE C:\FEC\TESTS\M5\BAD-DES.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M5\RUNTIME.FE C:\FEC\TESTS\M5\RUNTIME.FE > nul +if errorlevel 1 goto stage_fail +copy D:\FEC\TESTS\M5\RUNTIME.C C:\FEC\TESTS\M5\RUNTIME.C > nul +if errorlevel 1 goto stage_fail call C:\FEC\TEST-DOS.BAT if exist C:\FEC\TEST.OK goto vm_success From f662c4b8f7f31d0260e5bfa60956b936d53c48c8 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 14:37:20 +0900 Subject: [PATCH 018/184] chore: add 115200 baud DOS staging tools --- tools/dos_stage.py | 71 +++++++++++++ tools/dosagent.c | 243 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 tools/dos_stage.py create mode 100644 tools/dosagent.c diff --git a/tools/dos_stage.py b/tools/dos_stage.py new file mode 100644 index 0000000..b022d13 --- /dev/null +++ b/tools/dos_stage.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Stage changed source files into the running FreeDOS VM over DOSAGENT. + +Usage: python tools/dos_stage.py host/file=C:\\FEC\\DEST.FILE [...] +The DOS agent uses a 115200-baud virtual COM1 link and accepts at most 4096 +binary payload bytes per ASCII-hex WRITE request. +""" +import hashlib +import socket +import sys +from pathlib import Path + +HOST = "127.0.0.1" +PORT = 5555 +CHUNK_SIZE = 4096 + + +def request(line: str) -> str: + with socket.create_connection((HOST, PORT), timeout=30) as sock: + sock.sendall((line + "\n").encode("ascii")) + response = bytearray() + while not response.endswith(b"\n"): + part = sock.recv(65536) + if not part: + break + response.extend(part) + text = response.decode("ascii", "strict").strip() + if text.startswith("ERR "): + raise RuntimeError(bytes.fromhex(text[4:]).decode("utf-8", "replace")) + if not text.startswith("OK"): + raise RuntimeError("unexpected DOSAGENT response: " + text) + return text + + +def stage(local: Path, remote: str) -> None: + data = local.read_bytes() + remote_hex = remote.encode("ascii").hex().upper() + for offset in range(0, max(1, len(data)), CHUNK_SIZE): + part = data[offset : offset + CHUNK_SIZE] + mode = "T" if offset == 0 else "A" + request("WRITE %s %s %s" % (remote_hex, mode, part.hex().upper())) + remote_data = bytearray() + offset = 0 + while True: + response = request("READ %s %u" % (remote_hex, offset)) + _, eof, payload = response.split(" ", 2) + piece = bytes.fromhex(payload) + remote_data.extend(piece) + offset += len(piece) + if eof == "1": + break + if bytes(remote_data) != data: + raise RuntimeError("verification mismatch for " + remote) + print("staged %s -> %s (%d bytes, sha256 %s)" % ( + local, remote, len(data), hashlib.sha256(data).hexdigest())) + + +def main() -> int: + if len(sys.argv) < 2: + print("usage: dos_stage.py LOCAL=DOS_PATH [... ]", file=sys.stderr) + return 2 + for spec in sys.argv[1:]: + if "=" not in spec: + raise SystemExit("missing '=' in " + spec) + local, remote = spec.split("=", 1) + stage(Path(local), remote) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/dosagent.c b/tools/dosagent.c new file mode 100644 index 0000000..1c94073 --- /dev/null +++ b/tools/dosagent.c @@ -0,0 +1,243 @@ +#include +#include +#include +#include +#include +#include + +#define LINE_SIZE 9000 +#define CHUNK_SIZE 4096 + +static char linebuf[LINE_SIZE]; +static unsigned char data[CHUNK_SIZE + 1]; + +#define COM1_BASE 0x3F8 +/* The QEMU backend is a TCP socket; use the fastest standard 16550 rate. + 1.8432 MHz / (16 * 1) = 115200 baud. */ +#define COM1_DIVISOR 1 + +static void serial_init(void) { + outp(COM1_BASE + 1, 0x00); + outp(COM1_BASE + 3, 0x80); + outp(COM1_BASE + 0, COM1_DIVISOR); + outp(COM1_BASE + 1, 0); + outp(COM1_BASE + 3, 0x03); + outp(COM1_BASE + 2, 0xC7); + outp(COM1_BASE + 4, 0x0B); +} + +static int console_break_requested(void) { + unsigned short key = _bios_keybrd(_KEYBRD_READY); + if ((key & 0xFF) != 3) return 0; + _bios_keybrd(_KEYBRD_READ); + return 1; +} + +static int serial_getc(void) { + while ((inp(COM1_BASE + 5) & 0x01) == 0) { + if (console_break_requested()) return -1; + } + return inp(COM1_BASE); +} + +static void serial_putc(int c) { + while ((inp(COM1_BASE + 5) & 0x20) == 0) { } + outp(COM1_BASE, c); +} + +static void serial_write(const char *text) { + while (*text) serial_putc((unsigned char)*text++); +} + +static int serial_readline(char *buffer, int cap) { + int used = 0; + int c; + for (;;) { + c = serial_getc(); + if (c < 0) return -1; + if (c == '\r' || c == '\n') { + if (used == 0) continue; + buffer[used] = '\0'; + return used; + } + if (used < cap - 1) buffer[used++] = (char)c; + } +} + +static int hexval(int c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + return -1; +} + +static int decode_hex(const char *src, unsigned char *dst, int cap) { + int n = 0; + int hi, lo; + while (*src && src[1]) { + if (n >= cap) return -1; + hi = hexval((unsigned char)src[0]); + lo = hexval((unsigned char)src[1]); + if (hi < 0 || lo < 0) return -1; + dst[n++] = (unsigned char)((hi << 4) | lo); + src += 2; + } + return *src ? -1 : n; +} + +static void print_hex(const unsigned char *src, unsigned count) { + static const char digits[] = "0123456789ABCDEF"; + unsigned i; + for (i = 0; i < count; ++i) { + serial_putc(digits[src[i] >> 4]); + serial_putc(digits[src[i] & 15]); + } +} + +static void ok_data(const unsigned char *src, unsigned count) { + serial_write("OK "); + print_hex(src, count); + serial_write("\r\n"); +} + +static void error_text(const char *message) { + serial_write("ERR "); + print_hex((const unsigned char *)message, strlen(message)); + serial_write("\r\n"); +} + +static int decode_path(const char *hex, char *path, int cap) { + int n = decode_hex(hex, (unsigned char *)path, cap - 1); + if (n < 0) return 0; + path[n] = '\0'; + return 1; +} + +static void command_read(char *args) { + char path[260]; + char *offset_text = strchr(args, ' '); + FILE *file; + long offset; + size_t count; + int eof; + + if (!offset_text) { error_text("READ requires path and offset"); return; } + *offset_text++ = '\0'; + if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } + offset = atol(offset_text); + file = fopen(path, "rb"); + if (!file) { error_text("Cannot open file"); return; } + if (fseek(file, offset, SEEK_SET) != 0) { fclose(file); error_text("Cannot seek file"); return; } + count = fread(data, 1, CHUNK_SIZE, file); + eof = feof(file) ? 1 : 0; + if (count < CHUNK_SIZE) eof = 1; + fclose(file); + sprintf(linebuf, "OK %d ", eof); + serial_write(linebuf); + print_hex(data, count); + serial_write("\r\n"); +} + +static void command_write(char *args) { + char path[260]; + char *mode_text = strchr(args, ' '); + char *payload; + FILE *file; + int count; + + if (!mode_text) { error_text("WRITE requires path, mode and data"); return; } + *mode_text++ = '\0'; + payload = strchr(mode_text, ' '); + if (!payload) { error_text("WRITE requires data"); return; } + *payload++ = '\0'; + if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } + count = decode_hex(payload, data, CHUNK_SIZE); + if (count < 0) { error_text("Invalid data encoding"); return; } + file = fopen(path, mode_text[0] == 'A' ? "ab" : "wb"); + if (!file) { error_text("Cannot write file"); return; } + if (count && fwrite(data, 1, count, file) != (size_t)count) { + fclose(file); error_text("Short write"); return; + } + fclose(file); + ok_data((const unsigned char *)"", 0); +} + +static void command_list(char *args) { + char path[260]; + char pattern[300]; + char output[CHUNK_SIZE]; + struct find_t found; + unsigned used = 0; + int rc; + + if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } + strcpy(pattern, path); + if (pattern[0] && pattern[strlen(pattern) - 1] != '\\' && pattern[strlen(pattern) - 1] != '/') strcat(pattern, "\\"); + strcat(pattern, "*.*"); + rc = _dos_findfirst(pattern, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_SUBDIR | _A_ARCH, &found); + while (rc == 0) { + char entry[100]; + int len; + if (strcmp(found.name, ".") && strcmp(found.name, "..")) { + sprintf(entry, "%s\t%lu\t%s\n", found.name, found.size, (found.attrib & _A_SUBDIR) ? "DIR" : "FILE"); + len = strlen(entry); + if (used + len >= sizeof(output)) break; + memcpy(output + used, entry, len); + used += len; + } + rc = _dos_findnext(&found); + } + ok_data((const unsigned char *)output, used); +} + +static void command_exec(char *args) { + char command[700]; + char shell_command[800]; + const char *temp_path = "C:\\PIEXEC.TMP"; + FILE *file; + int n, exit_code; + size_t count; + + n = decode_hex(args, (unsigned char *)command, sizeof(command) - 1); + if (n < 0) { error_text("Invalid command encoding"); return; } + command[n] = '\0'; + sprintf(shell_command, "COMMAND.COM /C %s > %s", command, temp_path); + exit_code = system(shell_command); + file = fopen(temp_path, "rb"); + count = file ? fread(data, 1, CHUNK_SIZE, file) : 0; + if (file) fclose(file); + remove(temp_path); + sprintf(linebuf, "OK %d ", exit_code); + serial_write(linebuf); + print_hex(data, count); + serial_write("\r\n"); +} + +int main(void) { + char *command; + char *args; + + serial_init(); + cprintf("\r\n[DOSAGENT] Ready on COM1. Press Ctrl+C here to stop.\r\n"); + serial_write("DOSAGENT READY\r\n"); + for (;;) { + if (serial_readline(linebuf, sizeof(linebuf)) < 0) { + cprintf("\r\n[DOSAGENT] Stopped from VGA console.\r\n"); + break; + } + command = linebuf; + args = strchr(command, ' '); + if (args) *args++ = '\0'; else args = command + strlen(command); + + cprintf("[DOSAGENT] %s\r\n", command); + + if (!strcmp(command, "PING")) ok_data((const unsigned char *)"PONG", 4); + else if (!strcmp(command, "READ")) command_read(args); + else if (!strcmp(command, "WRITE")) command_write(args); + else if (!strcmp(command, "LIST")) command_list(args); + else if (!strcmp(command, "EXEC")) command_exec(args); + else if (!strcmp(command, "QUIT")) { ok_data((const unsigned char *)"BYE", 3); break; } + else error_text("Unknown command"); + } + return 0; +} From e5781fc168988ff8be1361f3ecf099686b363671 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 14:46:33 +0900 Subject: [PATCH 019/184] feat: add TCP staging path for FreeDOS --- fec/tcp-stage.bat | 19 +++++++++++++ tools/tcp_stage.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 fec/tcp-stage.bat create mode 100644 tools/tcp_stage.py diff --git a/fec/tcp-stage.bat b/fec/tcp-stage.bat new file mode 100644 index 0000000..d4825c5 --- /dev/null +++ b/fec/tcp-stage.bat @@ -0,0 +1,19 @@ +@echo off +rem Pull a host-built TCP staging bundle into the authoritative C:\FEC tree. +rem QEMU user networking exposes the host as 10.0.2.2. HTGET and UNZIP are +rem supplied by the installed FreeDOS network tools. +if "%1"=="" goto usage +if exist C:\FEC\STAGE.ZIP del C:\FEC\STAGE.ZIP +htget -o C:\FEC\STAGE.ZIP %1 +if errorlevel 1 goto fail +unzip -o C:\FEC\STAGE.ZIP -d C:\FEC +if errorlevel 1 goto fail +if exist C:\FEC\TCPSTG.OK del C:\FEC\TCPSTG.OK +echo OK>C:\FEC\TCPSTG.OK +goto done +:usage +echo usage: TCP-STAGE.BAT http://10.0.2.2:8000/STAGE.ZIP +goto done +:fail +echo FAIL>C:\FEC\TCPSTG.FAIL +:done diff --git a/tools/tcp_stage.py b/tools/tcp_stage.py new file mode 100644 index 0000000..f29647d --- /dev/null +++ b/tools/tcp_stage.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Build and serve a FreeDOS TCP staging bundle. + +The guest pulls STAGE.ZIP with its existing HTGET client from QEMU user-net's +host address (10.0.2.2), then extracts it into C:\\FEC. This avoids changing +the live vvfat exchange disk and keeps serial for bootstrap/recovery only. +""" +import argparse +import http.server +import os +import shutil +import zipfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_STAGE = ROOT / ".tcp-stage" + + +def bundle(stage: Path, files: list[str]) -> Path: + if stage.exists(): + shutil.rmtree(stage) + stage.mkdir(parents=True) + archive = stage / "STAGE.ZIP" + with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf: + for item in files: + source = (ROOT / item).resolve() + try: + relative = source.relative_to(ROOT / "fec") + except ValueError as exc: + raise SystemExit("only files below fec/ may be staged: " + item) from exc + if not source.is_file(): + raise SystemExit("not a file: " + item) + zf.write(source, relative.as_posix()) + print("created %s (%d bytes)" % (archive, archive.stat().st_size)) + return archive + + +def serve(stage: Path, port: int) -> None: + if not (stage / "STAGE.ZIP").is_file(): + raise SystemExit("missing STAGE.ZIP; run bundle first") + handler = lambda *args, **kwargs: http.server.SimpleHTTPRequestHandler( + *args, directory=str(stage), **kwargs) + server = http.server.ThreadingHTTPServer(("0.0.0.0", port), handler) + print("serving %s at http://10.0.2.2:%d/STAGE.ZIP" % (stage, port)) + try: + server.serve_forever() + finally: + server.server_close() + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--stage-dir", type=Path, default=DEFAULT_STAGE) + commands = parser.add_subparsers(dest="command", required=True) + make = commands.add_parser("bundle") + make.add_argument("files", nargs="+") + web = commands.add_parser("serve") + web.add_argument("--port", type=int, default=8000) + args = parser.parse_args() + if args.command == "bundle": + bundle(args.stage_dir, args.files) + else: + serve(args.stage_dir, args.port) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 8e8ab22d30648e77b9f38204cf603e47d4f9320e Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 15:51:42 +0900 Subject: [PATCH 020/184] feat: replace serial automation with resident TCP agent --- .qemu/reset.ps1 | 68 ++++++++++ .qemu/run.ps1 | 15 +-- .qemu/serial-relay.mjs | 110 ---------------- .qemu/share/dosagent.c | 240 ----------------------------------- .qemu/tcp-agent-relay.mjs | 71 +++++++++++ fec/tcp-stage.bat | 19 --- tools/dos_stage.py | 67 ++++++++-- tools/dosagent.c | 243 ------------------------------------ tools/tcp_stage.py | 68 ---------- tools/tcpagent/BUILD.BAT | 6 + tools/tcpagent/INSTALL.BAT | 15 +++ tools/tcpagent/Makefile | 38 ++++++ tools/tcpagent/README.md | 32 +++++ tools/tcpagent/WPP.RSP | 11 ++ tools/tcpagent/tcpagent.cfg | 12 ++ tools/tcpagent/tcpagent.cpp | 205 ++++++++++++++++++++++++++++++ 16 files changed, 521 insertions(+), 699 deletions(-) create mode 100644 .qemu/reset.ps1 delete mode 100644 .qemu/serial-relay.mjs delete mode 100644 .qemu/share/dosagent.c create mode 100644 .qemu/tcp-agent-relay.mjs delete mode 100644 fec/tcp-stage.bat delete mode 100644 tools/dosagent.c delete mode 100644 tools/tcp_stage.py create mode 100644 tools/tcpagent/BUILD.BAT create mode 100644 tools/tcpagent/INSTALL.BAT create mode 100644 tools/tcpagent/Makefile create mode 100644 tools/tcpagent/README.md create mode 100644 tools/tcpagent/WPP.RSP create mode 100644 tools/tcpagent/tcpagent.cfg create mode 100644 tools/tcpagent/tcpagent.cpp diff --git a/.qemu/reset.ps1 b/.qemu/reset.ps1 new file mode 100644 index 0000000..f2465ac --- /dev/null +++ b/.qemu/reset.ps1 @@ -0,0 +1,68 @@ +param( + [int] $ReadyTimeoutSeconds = 45 +) + +$ErrorActionPreference = 'Stop' +$root = Split-Path -Parent $MyInvocation.MyCommand.Path + +function Stop-Listener([int] $Port) { + $listeners = Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue + foreach ($listener in $listeners) { + Stop-Process -Id $listener.OwningProcess -Force -ErrorAction SilentlyContinue + } +} + +# Quit through QEMU's monitor so qcow2/FAT writes are flushed. Forced VM +# termination is deliberately forbidden because it previously lost files. +$monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue +if ($monitor) { + & (Join-Path $root 'monitor.ps1') 'quit' | Out-Null + $deadline = (Get-Date).AddSeconds(10) + do { + Start-Sleep -Milliseconds 250 + $monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue + } while ($monitor -and (Get-Date) -lt $deadline) + if ($monitor) { throw 'QEMU did not quit cleanly; refusing a forced reset.' } +} +Stop-Listener 5555 +Start-Sleep -Milliseconds 500 + +$runner = Join-Path $root 'run.ps1' +Start-Process powershell -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',$runner) -WorkingDirectory $root +$deadline = (Get-Date).AddSeconds(15) +do { + Start-Sleep -Milliseconds 250 + $monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue +} while (-not $monitor -and (Get-Date) -lt $deadline) +if (-not $monitor) { throw 'QEMU monitor did not start.' } + +# Select the default boot entry. TCPAGENT is started by FDAUTO.BAT after the +# packet driver, so readiness is the agent connection/PONG rather than sleep. +Start-Sleep -Seconds 2 +& (Join-Path $root 'monitor.ps1') 'sendkey ret' | Out-Null +$deadline = (Get-Date).AddSeconds($ReadyTimeoutSeconds) +$ready = $false +do { + Start-Sleep -Milliseconds 500 + $connection = Get-NetTCPConnection -State Established -LocalPort 5558 -ErrorAction SilentlyContinue + if ($connection) { + try { + $client = [Net.Sockets.TcpClient]::new('127.0.0.1',5555) + $stream = $client.GetStream() + $stream.ReadTimeout = 1500 + $bytes = [Text.Encoding]::ASCII.GetBytes("PING`n") + $stream.Write($bytes,0,$bytes.Length) + $buffer = New-Object byte[] 128 + $reply = '' + while ($reply -notmatch "`n") { + $count = $stream.Read($buffer,0,$buffer.Length) + if ($count -le 0) { break } + $reply += [Text.Encoding]::ASCII.GetString($buffer,0,$count) + } + $client.Dispose() + $ready = $reply -match '^OK 504F4E47' + } catch { $ready = $false } + } +} while (-not $ready -and (Get-Date) -lt $deadline) +if (-not $ready) { throw 'TCPAGENT did not become ready.' } +Write-Host 'QEMU reset complete; TCPAGENT answered PONG.' diff --git a/.qemu/run.ps1 b/.qemu/run.ps1 index 865e496..eefe499 100644 --- a/.qemu/run.ps1 +++ b/.qemu/run.ps1 @@ -4,26 +4,21 @@ $ErrorActionPreference = 'Stop' $qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop $node = Get-Command node.exe -ErrorAction Stop $disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$share = Join-Path $PSScriptRoot 'share' -$relay = Join-Path $PSScriptRoot 'serial-relay.mjs' - -# QEMU's FAT-directory drive is a convenient exchange volume, but it is not a -# reliable live-sync mechanism. Restart QEMU after host-side source changes. -New-Item -ItemType Directory -Force -Path $share | Out-Null +$relay = Join-Path $PSScriptRoot 'tcp-agent-relay.mjs' if (-not (Test-Path -LiteralPath $disk)) { throw "Missing $disk. Run .qemu\\setup.ps1, then .qemu\\install.ps1." } -$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5556 -ErrorAction SilentlyContinue +$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5558 -ErrorAction SilentlyContinue if (-not $relayListening) { Start-Process -FilePath $node.Source -ArgumentList @($relay) -WorkingDirectory $PSScriptRoot -WindowStyle Hidden $deadline = [DateTime]::UtcNow.AddSeconds(5) do { Start-Sleep -Milliseconds 100 - $relayListening = Get-NetTCPConnection -State Listen -LocalPort 5556 -ErrorAction SilentlyContinue + $relayListening = Get-NetTCPConnection -State Listen -LocalPort 5558 -ErrorAction SilentlyContinue } while (-not $relayListening -and [DateTime]::UtcNow -lt $deadline) - if (-not $relayListening) { throw 'Serial relay did not start.' } + if (-not $relayListening) { throw 'TCP agent relay did not start.' } } $monitorListening = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue @@ -36,9 +31,7 @@ if ($monitorListening) { -smp 1 ` -m 64 ` -drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" ` - -hdb "fat:rw:$share" ` -nic user,model=ne2k_isa ` -monitor "tcp:127.0.0.1:4444,server=on,wait=off" ` - -serial "tcp:127.0.0.1:5556" ` -boot order=c ` -display default diff --git a/.qemu/serial-relay.mjs b/.qemu/serial-relay.mjs deleted file mode 100644 index de44160..0000000 --- a/.qemu/serial-relay.mjs +++ /dev/null @@ -1,110 +0,0 @@ -import net from "node:net"; - -const HOST = "127.0.0.1"; -const QEMU_PORT = 5556; -const TOOL_PORT = 5555; -const OBSERVER_PORT = 5557; - -let qemu = null; -let controller = null; -const observers = new Set(); - -function safeWrite(socket, data) { - if (socket && !socket.destroyed && socket.writable) socket.write(data); -} - -function decodeHex(hex) { - if (!hex || hex.length % 2 || !/^[0-9a-f]+$/i.test(hex)) return null; - return Buffer.from(hex, "hex").toString("utf8"); -} - -function visible(text) { - const shortened = text.length > 400 ? `${text.slice(0, 400)}…` : text; - return shortened - .replace(/\r\n?/g, "\n") - .replace(/[^\x20-\x7E\n]/g, (char) => `[0x${char.charCodeAt(0).toString(16).padStart(2, "0")}]`) - .replace(/\n/g, "\r\n "); -} - -function formatLine(source, line) { - if (!line) return null; - if (line === "PING" || line === "QUIT" || line === "DOSAGENT READY") return `${source} ${line}`; - - const [command, ...args] = line.split(" "); - if (["READ", "LIST"].includes(command) && args[0]) { - return `${source} ${command} ${visible(decodeHex(args[0]) ?? args[0])}${args[1] ? ` @${args[1]}` : ""}`; - } - if (command === "WRITE" && args.length >= 3) { - const path = decodeHex(args[0]) ?? args[0]; - const body = decodeHex(args.slice(2).join(" ")) ?? args.slice(2).join(" "); - return `${source} WRITE ${visible(path)} (${args[1] === "T" ? "replace" : "append"}) ${visible(body)}`; - } - if (command === "EXEC" && args[0]) return `${source} EXEC ${visible(decodeHex(args.join(" ")) ?? args.join(" "))}`; - if (command === "ERR" && args[0]) return `${source} ERROR ${visible(decodeHex(args.join(" ")) ?? args.join(" "))}`; - if (command === "OK") { - const singlePayload = args.length === 1 ? decodeHex(args[0]) : null; - const payload = singlePayload ?? (args.length > 1 ? decodeHex(args.slice(1).join(" ")) : null); - const status = singlePayload !== null ? "" : (args[0] ? ` (${args[0]})` : ""); - return `${source} OK${status}${payload !== null ? ` ${visible(payload)}` : ""}`; - } - return `${source} ${line}`; -} - -const traceBuffers = new Map(); -function trace(source, data) { - const pending = (traceBuffers.get(source) ?? "") + data.toString("ascii"); - const parts = pending.split(/\r?\n/); - traceBuffers.set(source, parts.pop()); - for (const line of parts) { - const formatted = formatLine(source, line); - if (formatted) for (const observer of observers) safeWrite(observer, `${formatted}\r\n`); - } -} - -const qemuServer = net.createServer((socket) => { - if (qemu && !qemu.destroyed) { - socket.end(); - return; - } - qemu = socket; - socket.setNoDelay(true); - socket.on("data", (data) => { - safeWrite(controller, data); - trace("DOS -> Pi", data); - }); - socket.on("close", () => { - if (qemu === socket) qemu = null; - if (controller && !controller.destroyed) controller.destroy(); - }); - socket.on("error", () => {}); -}); - -const toolServer = net.createServer((socket) => { - if (controller && !controller.destroyed) { - socket.end("ERR 53455249414C20434F4E54524F4C4C45522042555359\r\n"); - return; - } - controller = socket; - socket.setNoDelay(true); - socket.on("data", (data) => { - safeWrite(qemu, data); - trace("Pi -> DOS", data); - }); - socket.on("close", () => { - if (controller === socket) controller = null; - }); - socket.on("error", () => {}); -}); - -const observerServer = net.createServer((socket) => { - observers.add(socket); - socket.setNoDelay(true); - socket.write("DOS serial observer - decoded protocol (read-only)\r\n"); - socket.on("data", () => {}); - socket.on("close", () => observers.delete(socket)); - socket.on("error", () => observers.delete(socket)); -}); - -qemuServer.listen(QEMU_PORT, HOST); -toolServer.listen(TOOL_PORT, HOST); -observerServer.listen(OBSERVER_PORT, HOST); diff --git a/.qemu/share/dosagent.c b/.qemu/share/dosagent.c deleted file mode 100644 index 01095b4..0000000 --- a/.qemu/share/dosagent.c +++ /dev/null @@ -1,240 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#define LINE_SIZE 9000 -#define CHUNK_SIZE 4096 - -static char linebuf[LINE_SIZE]; -static unsigned char data[CHUNK_SIZE + 1]; - -#define COM1_BASE 0x3F8 - -static void serial_init(void) { - outp(COM1_BASE + 1, 0x00); - outp(COM1_BASE + 3, 0x80); - outp(COM1_BASE + 0, 12); - outp(COM1_BASE + 1, 0); - outp(COM1_BASE + 3, 0x03); - outp(COM1_BASE + 2, 0xC7); - outp(COM1_BASE + 4, 0x0B); -} - -static int console_break_requested(void) { - unsigned short key = _bios_keybrd(_KEYBRD_READY); - if ((key & 0xFF) != 3) return 0; - _bios_keybrd(_KEYBRD_READ); - return 1; -} - -static int serial_getc(void) { - while ((inp(COM1_BASE + 5) & 0x01) == 0) { - if (console_break_requested()) return -1; - } - return inp(COM1_BASE); -} - -static void serial_putc(int c) { - while ((inp(COM1_BASE + 5) & 0x20) == 0) { } - outp(COM1_BASE, c); -} - -static void serial_write(const char *text) { - while (*text) serial_putc((unsigned char)*text++); -} - -static int serial_readline(char *buffer, int cap) { - int used = 0; - int c; - for (;;) { - c = serial_getc(); - if (c < 0) return -1; - if (c == '\r' || c == '\n') { - if (used == 0) continue; - buffer[used] = '\0'; - return used; - } - if (used < cap - 1) buffer[used++] = (char)c; - } -} - -static int hexval(int c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - return -1; -} - -static int decode_hex(const char *src, unsigned char *dst, int cap) { - int n = 0; - int hi, lo; - while (*src && src[1]) { - if (n >= cap) return -1; - hi = hexval((unsigned char)src[0]); - lo = hexval((unsigned char)src[1]); - if (hi < 0 || lo < 0) return -1; - dst[n++] = (unsigned char)((hi << 4) | lo); - src += 2; - } - return *src ? -1 : n; -} - -static void print_hex(const unsigned char *src, unsigned count) { - static const char digits[] = "0123456789ABCDEF"; - unsigned i; - for (i = 0; i < count; ++i) { - serial_putc(digits[src[i] >> 4]); - serial_putc(digits[src[i] & 15]); - } -} - -static void ok_data(const unsigned char *src, unsigned count) { - serial_write("OK "); - print_hex(src, count); - serial_write("\r\n"); -} - -static void error_text(const char *message) { - serial_write("ERR "); - print_hex((const unsigned char *)message, strlen(message)); - serial_write("\r\n"); -} - -static int decode_path(const char *hex, char *path, int cap) { - int n = decode_hex(hex, (unsigned char *)path, cap - 1); - if (n < 0) return 0; - path[n] = '\0'; - return 1; -} - -static void command_read(char *args) { - char path[260]; - char *offset_text = strchr(args, ' '); - FILE *file; - long offset; - size_t count; - int eof; - - if (!offset_text) { error_text("READ requires path and offset"); return; } - *offset_text++ = '\0'; - if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } - offset = atol(offset_text); - file = fopen(path, "rb"); - if (!file) { error_text("Cannot open file"); return; } - if (fseek(file, offset, SEEK_SET) != 0) { fclose(file); error_text("Cannot seek file"); return; } - count = fread(data, 1, CHUNK_SIZE, file); - eof = feof(file) ? 1 : 0; - if (count < CHUNK_SIZE) eof = 1; - fclose(file); - sprintf(linebuf, "OK %d ", eof); - serial_write(linebuf); - print_hex(data, count); - serial_write("\r\n"); -} - -static void command_write(char *args) { - char path[260]; - char *mode_text = strchr(args, ' '); - char *payload; - FILE *file; - int count; - - if (!mode_text) { error_text("WRITE requires path, mode and data"); return; } - *mode_text++ = '\0'; - payload = strchr(mode_text, ' '); - if (!payload) { error_text("WRITE requires data"); return; } - *payload++ = '\0'; - if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } - count = decode_hex(payload, data, CHUNK_SIZE); - if (count < 0) { error_text("Invalid data encoding"); return; } - file = fopen(path, mode_text[0] == 'A' ? "ab" : "wb"); - if (!file) { error_text("Cannot write file"); return; } - if (count && fwrite(data, 1, count, file) != (size_t)count) { - fclose(file); error_text("Short write"); return; - } - fclose(file); - ok_data((const unsigned char *)"", 0); -} - -static void command_list(char *args) { - char path[260]; - char pattern[300]; - char output[CHUNK_SIZE]; - struct find_t found; - unsigned used = 0; - int rc; - - if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } - strcpy(pattern, path); - if (pattern[0] && pattern[strlen(pattern) - 1] != '\\' && pattern[strlen(pattern) - 1] != '/') strcat(pattern, "\\"); - strcat(pattern, "*.*"); - rc = _dos_findfirst(pattern, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_SUBDIR | _A_ARCH, &found); - while (rc == 0) { - char entry[100]; - int len; - if (strcmp(found.name, ".") && strcmp(found.name, "..")) { - sprintf(entry, "%s\t%lu\t%s\n", found.name, found.size, (found.attrib & _A_SUBDIR) ? "DIR" : "FILE"); - len = strlen(entry); - if (used + len >= sizeof(output)) break; - memcpy(output + used, entry, len); - used += len; - } - rc = _dos_findnext(&found); - } - ok_data((const unsigned char *)output, used); -} - -static void command_exec(char *args) { - char command[700]; - char shell_command[800]; - const char *temp_path = "C:\\PIEXEC.TMP"; - FILE *file; - int n, exit_code; - size_t count; - - n = decode_hex(args, (unsigned char *)command, sizeof(command) - 1); - if (n < 0) { error_text("Invalid command encoding"); return; } - command[n] = '\0'; - sprintf(shell_command, "COMMAND.COM /C %s > %s", command, temp_path); - exit_code = system(shell_command); - file = fopen(temp_path, "rb"); - count = file ? fread(data, 1, CHUNK_SIZE, file) : 0; - if (file) fclose(file); - remove(temp_path); - sprintf(linebuf, "OK %d ", exit_code); - serial_write(linebuf); - print_hex(data, count); - serial_write("\r\n"); -} - -int main(void) { - char *command; - char *args; - - serial_init(); - cprintf("\r\n[DOSAGENT] Ready on COM1. Press Ctrl+C here to stop.\r\n"); - serial_write("DOSAGENT READY\r\n"); - for (;;) { - if (serial_readline(linebuf, sizeof(linebuf)) < 0) { - cprintf("\r\n[DOSAGENT] Stopped from VGA console.\r\n"); - break; - } - command = linebuf; - args = strchr(command, ' '); - if (args) *args++ = '\0'; else args = command + strlen(command); - - cprintf("[DOSAGENT] %s\r\n", command); - - if (!strcmp(command, "PING")) ok_data((const unsigned char *)"PONG", 4); - else if (!strcmp(command, "READ")) command_read(args); - else if (!strcmp(command, "WRITE")) command_write(args); - else if (!strcmp(command, "LIST")) command_list(args); - else if (!strcmp(command, "EXEC")) command_exec(args); - else if (!strcmp(command, "QUIT")) { ok_data((const unsigned char *)"BYE", 3); break; } - else error_text("Unknown command"); - } - return 0; -} diff --git a/.qemu/tcp-agent-relay.mjs b/.qemu/tcp-agent-relay.mjs new file mode 100644 index 0000000..2768bc4 --- /dev/null +++ b/.qemu/tcp-agent-relay.mjs @@ -0,0 +1,71 @@ +import net from "node:net"; + +const HOST = "127.0.0.1"; +const TOOL_PORT = 5555; +const OBSERVER_PORT = 5557; +const TCP_AGENT_PORT = 5558; + +let tcpAgent = null; +let controller = null; +const observers = new Set(); + +function safeWrite(socket, data) { + if (socket && !socket.destroyed && socket.writable) socket.write(data); +} + +function trace(source, data) { + const text = data.toString("ascii"); + const shortened = text.length > 400 ? `${text.slice(0, 400)}…` : text; + for (const observer of observers) safeWrite(observer, `${source} ${shortened}\r\n`); +} + +const tcpAgentServer = net.createServer((socket) => { + if (tcpAgent && !tcpAgent.destroyed) { + socket.end(); + return; + } + tcpAgent = socket; + socket.setNoDelay(true); + socket.on("data", (data) => { + safeWrite(controller, data); + trace("DOS->Pi", data); + }); + socket.on("close", () => { + if (tcpAgent === socket) tcpAgent = null; + }); + socket.on("error", () => {}); +}); + +const toolServer = net.createServer((socket) => { + if (controller && !controller.destroyed) { + socket.end("ERR 544350204147454E5420434F4E54524F4C4C45522042555359\r\n"); + return; + } + if (!tcpAgent || tcpAgent.destroyed) { + socket.end("ERR 544350204147454E54204E4F5420434F4E4E4543544544\r\n"); + return; + } + controller = socket; + socket.setNoDelay(true); + socket.on("data", (data) => { + safeWrite(tcpAgent, data); + trace("Pi->DOS", data); + }); + socket.on("close", () => { + if (controller === socket) controller = null; + }); + socket.on("error", () => {}); +}); + +const observerServer = net.createServer((socket) => { + observers.add(socket); + socket.setNoDelay(true); + socket.write("DOS TCP agent observer (read-only)\r\n"); + socket.on("data", () => {}); + socket.on("close", () => observers.delete(socket)); + socket.on("error", () => observers.delete(socket)); +}); + +tcpAgentServer.listen(TCP_AGENT_PORT, HOST); +toolServer.listen(TOOL_PORT, HOST); +observerServer.listen(OBSERVER_PORT, HOST); diff --git a/fec/tcp-stage.bat b/fec/tcp-stage.bat deleted file mode 100644 index d4825c5..0000000 --- a/fec/tcp-stage.bat +++ /dev/null @@ -1,19 +0,0 @@ -@echo off -rem Pull a host-built TCP staging bundle into the authoritative C:\FEC tree. -rem QEMU user networking exposes the host as 10.0.2.2. HTGET and UNZIP are -rem supplied by the installed FreeDOS network tools. -if "%1"=="" goto usage -if exist C:\FEC\STAGE.ZIP del C:\FEC\STAGE.ZIP -htget -o C:\FEC\STAGE.ZIP %1 -if errorlevel 1 goto fail -unzip -o C:\FEC\STAGE.ZIP -d C:\FEC -if errorlevel 1 goto fail -if exist C:\FEC\TCPSTG.OK del C:\FEC\TCPSTG.OK -echo OK>C:\FEC\TCPSTG.OK -goto done -:usage -echo usage: TCP-STAGE.BAT http://10.0.2.2:8000/STAGE.ZIP -goto done -:fail -echo FAIL>C:\FEC\TCPSTG.FAIL -:done diff --git a/tools/dos_stage.py b/tools/dos_stage.py index b022d13..1218749 100644 --- a/tools/dos_stage.py +++ b/tools/dos_stage.py @@ -12,7 +12,7 @@ from pathlib import Path HOST = "127.0.0.1" PORT = 5555 -CHUNK_SIZE = 4096 +DEFAULT_CHUNK_SIZE = 4096 def request(line: str) -> str: @@ -32,11 +32,50 @@ def request(line: str) -> str: return text -def stage(local: Path, remote: str) -> None: +def recv_line(sock: socket.socket) -> bytes: + line = bytearray() + while not line.endswith(b"\n"): + part = sock.recv(1) + if not part: + raise RuntimeError("TCP agent closed the connection") + line.extend(part) + return bytes(line).strip() + + +def fnv1a(data: bytes) -> int: + value = 2166136261 + for byte in data: + value = ((value ^ byte) * 16777619) & 0xFFFFFFFF + return value + + +def binary_stage(local: Path, remote: str) -> None: + data = local.read_bytes() + path_hex = remote.encode("ascii").hex().upper() + with socket.create_connection((HOST, PORT), timeout=30) as sock: + sock.sendall(("PUT %s %d\n" % (path_hex, len(data))).encode("ascii")) + sock.sendall(data) + response = recv_line(sock) + if not response.startswith(b"OK"): + raise RuntimeError("PUT failed: " + response.decode("ascii", "replace")) + sock.sendall(("HASH %s\n" % path_hex).encode("ascii")) + header = recv_line(sock).decode("ascii", "strict") + fields = header.split() + if len(fields) != 3 or fields[0] != "STAT": + raise RuntimeError("HASH failed: " + header) + remote_length = int(fields[1]) + remote_hash = int(fields[2], 16) + if remote_length != len(data) or remote_hash != fnv1a(data): + raise RuntimeError("verification mismatch for " + remote) + print("staged %s -> %s (%d bytes, sha256 %s, binary TCP)" % ( + local, remote, len(data), hashlib.sha256(data).hexdigest())) + + +def stage(local: Path, remote: str, chunk_size: int) -> None: data = local.read_bytes() remote_hex = remote.encode("ascii").hex().upper() - for offset in range(0, max(1, len(data)), CHUNK_SIZE): - part = data[offset : offset + CHUNK_SIZE] + for offset in range(0, max(1, len(data)), chunk_size): + part = data[offset : offset + chunk_size] mode = "T" if offset == 0 else "A" request("WRITE %s %s %s" % (remote_hex, mode, part.hex().upper())) remote_data = bytearray() @@ -56,14 +95,26 @@ def stage(local: Path, remote: str) -> None: def main() -> int: - if len(sys.argv) < 2: - print("usage: dos_stage.py LOCAL=DOS_PATH [... ]", file=sys.stderr) + args = sys.argv[1:] + chunk_size = DEFAULT_CHUNK_SIZE + binary = False + if args and args[0] == "--binary": + binary = True + args = args[1:] + if len(args) >= 2 and args[0] == "--chunk-size": + chunk_size = int(args[1]) + args = args[2:] + if not args: + print("usage: dos_stage.py [--binary] [--chunk-size N] LOCAL=DOS_PATH [... ]", file=sys.stderr) return 2 - for spec in sys.argv[1:]: + for spec in args: if "=" not in spec: raise SystemExit("missing '=' in " + spec) local, remote = spec.split("=", 1) - stage(Path(local), remote) + if binary: + binary_stage(Path(local), remote) + else: + stage(Path(local), remote, chunk_size) return 0 diff --git a/tools/dosagent.c b/tools/dosagent.c deleted file mode 100644 index 1c94073..0000000 --- a/tools/dosagent.c +++ /dev/null @@ -1,243 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#define LINE_SIZE 9000 -#define CHUNK_SIZE 4096 - -static char linebuf[LINE_SIZE]; -static unsigned char data[CHUNK_SIZE + 1]; - -#define COM1_BASE 0x3F8 -/* The QEMU backend is a TCP socket; use the fastest standard 16550 rate. - 1.8432 MHz / (16 * 1) = 115200 baud. */ -#define COM1_DIVISOR 1 - -static void serial_init(void) { - outp(COM1_BASE + 1, 0x00); - outp(COM1_BASE + 3, 0x80); - outp(COM1_BASE + 0, COM1_DIVISOR); - outp(COM1_BASE + 1, 0); - outp(COM1_BASE + 3, 0x03); - outp(COM1_BASE + 2, 0xC7); - outp(COM1_BASE + 4, 0x0B); -} - -static int console_break_requested(void) { - unsigned short key = _bios_keybrd(_KEYBRD_READY); - if ((key & 0xFF) != 3) return 0; - _bios_keybrd(_KEYBRD_READ); - return 1; -} - -static int serial_getc(void) { - while ((inp(COM1_BASE + 5) & 0x01) == 0) { - if (console_break_requested()) return -1; - } - return inp(COM1_BASE); -} - -static void serial_putc(int c) { - while ((inp(COM1_BASE + 5) & 0x20) == 0) { } - outp(COM1_BASE, c); -} - -static void serial_write(const char *text) { - while (*text) serial_putc((unsigned char)*text++); -} - -static int serial_readline(char *buffer, int cap) { - int used = 0; - int c; - for (;;) { - c = serial_getc(); - if (c < 0) return -1; - if (c == '\r' || c == '\n') { - if (used == 0) continue; - buffer[used] = '\0'; - return used; - } - if (used < cap - 1) buffer[used++] = (char)c; - } -} - -static int hexval(int c) { - if (c >= '0' && c <= '9') return c - '0'; - if (c >= 'A' && c <= 'F') return c - 'A' + 10; - if (c >= 'a' && c <= 'f') return c - 'a' + 10; - return -1; -} - -static int decode_hex(const char *src, unsigned char *dst, int cap) { - int n = 0; - int hi, lo; - while (*src && src[1]) { - if (n >= cap) return -1; - hi = hexval((unsigned char)src[0]); - lo = hexval((unsigned char)src[1]); - if (hi < 0 || lo < 0) return -1; - dst[n++] = (unsigned char)((hi << 4) | lo); - src += 2; - } - return *src ? -1 : n; -} - -static void print_hex(const unsigned char *src, unsigned count) { - static const char digits[] = "0123456789ABCDEF"; - unsigned i; - for (i = 0; i < count; ++i) { - serial_putc(digits[src[i] >> 4]); - serial_putc(digits[src[i] & 15]); - } -} - -static void ok_data(const unsigned char *src, unsigned count) { - serial_write("OK "); - print_hex(src, count); - serial_write("\r\n"); -} - -static void error_text(const char *message) { - serial_write("ERR "); - print_hex((const unsigned char *)message, strlen(message)); - serial_write("\r\n"); -} - -static int decode_path(const char *hex, char *path, int cap) { - int n = decode_hex(hex, (unsigned char *)path, cap - 1); - if (n < 0) return 0; - path[n] = '\0'; - return 1; -} - -static void command_read(char *args) { - char path[260]; - char *offset_text = strchr(args, ' '); - FILE *file; - long offset; - size_t count; - int eof; - - if (!offset_text) { error_text("READ requires path and offset"); return; } - *offset_text++ = '\0'; - if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } - offset = atol(offset_text); - file = fopen(path, "rb"); - if (!file) { error_text("Cannot open file"); return; } - if (fseek(file, offset, SEEK_SET) != 0) { fclose(file); error_text("Cannot seek file"); return; } - count = fread(data, 1, CHUNK_SIZE, file); - eof = feof(file) ? 1 : 0; - if (count < CHUNK_SIZE) eof = 1; - fclose(file); - sprintf(linebuf, "OK %d ", eof); - serial_write(linebuf); - print_hex(data, count); - serial_write("\r\n"); -} - -static void command_write(char *args) { - char path[260]; - char *mode_text = strchr(args, ' '); - char *payload; - FILE *file; - int count; - - if (!mode_text) { error_text("WRITE requires path, mode and data"); return; } - *mode_text++ = '\0'; - payload = strchr(mode_text, ' '); - if (!payload) { error_text("WRITE requires data"); return; } - *payload++ = '\0'; - if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } - count = decode_hex(payload, data, CHUNK_SIZE); - if (count < 0) { error_text("Invalid data encoding"); return; } - file = fopen(path, mode_text[0] == 'A' ? "ab" : "wb"); - if (!file) { error_text("Cannot write file"); return; } - if (count && fwrite(data, 1, count, file) != (size_t)count) { - fclose(file); error_text("Short write"); return; - } - fclose(file); - ok_data((const unsigned char *)"", 0); -} - -static void command_list(char *args) { - char path[260]; - char pattern[300]; - char output[CHUNK_SIZE]; - struct find_t found; - unsigned used = 0; - int rc; - - if (!decode_path(args, path, sizeof(path))) { error_text("Invalid path encoding"); return; } - strcpy(pattern, path); - if (pattern[0] && pattern[strlen(pattern) - 1] != '\\' && pattern[strlen(pattern) - 1] != '/') strcat(pattern, "\\"); - strcat(pattern, "*.*"); - rc = _dos_findfirst(pattern, _A_NORMAL | _A_RDONLY | _A_HIDDEN | _A_SYSTEM | _A_SUBDIR | _A_ARCH, &found); - while (rc == 0) { - char entry[100]; - int len; - if (strcmp(found.name, ".") && strcmp(found.name, "..")) { - sprintf(entry, "%s\t%lu\t%s\n", found.name, found.size, (found.attrib & _A_SUBDIR) ? "DIR" : "FILE"); - len = strlen(entry); - if (used + len >= sizeof(output)) break; - memcpy(output + used, entry, len); - used += len; - } - rc = _dos_findnext(&found); - } - ok_data((const unsigned char *)output, used); -} - -static void command_exec(char *args) { - char command[700]; - char shell_command[800]; - const char *temp_path = "C:\\PIEXEC.TMP"; - FILE *file; - int n, exit_code; - size_t count; - - n = decode_hex(args, (unsigned char *)command, sizeof(command) - 1); - if (n < 0) { error_text("Invalid command encoding"); return; } - command[n] = '\0'; - sprintf(shell_command, "COMMAND.COM /C %s > %s", command, temp_path); - exit_code = system(shell_command); - file = fopen(temp_path, "rb"); - count = file ? fread(data, 1, CHUNK_SIZE, file) : 0; - if (file) fclose(file); - remove(temp_path); - sprintf(linebuf, "OK %d ", exit_code); - serial_write(linebuf); - print_hex(data, count); - serial_write("\r\n"); -} - -int main(void) { - char *command; - char *args; - - serial_init(); - cprintf("\r\n[DOSAGENT] Ready on COM1. Press Ctrl+C here to stop.\r\n"); - serial_write("DOSAGENT READY\r\n"); - for (;;) { - if (serial_readline(linebuf, sizeof(linebuf)) < 0) { - cprintf("\r\n[DOSAGENT] Stopped from VGA console.\r\n"); - break; - } - command = linebuf; - args = strchr(command, ' '); - if (args) *args++ = '\0'; else args = command + strlen(command); - - cprintf("[DOSAGENT] %s\r\n", command); - - if (!strcmp(command, "PING")) ok_data((const unsigned char *)"PONG", 4); - else if (!strcmp(command, "READ")) command_read(args); - else if (!strcmp(command, "WRITE")) command_write(args); - else if (!strcmp(command, "LIST")) command_list(args); - else if (!strcmp(command, "EXEC")) command_exec(args); - else if (!strcmp(command, "QUIT")) { ok_data((const unsigned char *)"BYE", 3); break; } - else error_text("Unknown command"); - } - return 0; -} diff --git a/tools/tcp_stage.py b/tools/tcp_stage.py deleted file mode 100644 index f29647d..0000000 --- a/tools/tcp_stage.py +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env python3 -"""Build and serve a FreeDOS TCP staging bundle. - -The guest pulls STAGE.ZIP with its existing HTGET client from QEMU user-net's -host address (10.0.2.2), then extracts it into C:\\FEC. This avoids changing -the live vvfat exchange disk and keeps serial for bootstrap/recovery only. -""" -import argparse -import http.server -import os -import shutil -import zipfile -from pathlib import Path - -ROOT = Path(__file__).resolve().parent.parent -DEFAULT_STAGE = ROOT / ".tcp-stage" - - -def bundle(stage: Path, files: list[str]) -> Path: - if stage.exists(): - shutil.rmtree(stage) - stage.mkdir(parents=True) - archive = stage / "STAGE.ZIP" - with zipfile.ZipFile(archive, "w", zipfile.ZIP_DEFLATED) as zf: - for item in files: - source = (ROOT / item).resolve() - try: - relative = source.relative_to(ROOT / "fec") - except ValueError as exc: - raise SystemExit("only files below fec/ may be staged: " + item) from exc - if not source.is_file(): - raise SystemExit("not a file: " + item) - zf.write(source, relative.as_posix()) - print("created %s (%d bytes)" % (archive, archive.stat().st_size)) - return archive - - -def serve(stage: Path, port: int) -> None: - if not (stage / "STAGE.ZIP").is_file(): - raise SystemExit("missing STAGE.ZIP; run bundle first") - handler = lambda *args, **kwargs: http.server.SimpleHTTPRequestHandler( - *args, directory=str(stage), **kwargs) - server = http.server.ThreadingHTTPServer(("0.0.0.0", port), handler) - print("serving %s at http://10.0.2.2:%d/STAGE.ZIP" % (stage, port)) - try: - server.serve_forever() - finally: - server.server_close() - - -def main() -> int: - parser = argparse.ArgumentParser() - parser.add_argument("--stage-dir", type=Path, default=DEFAULT_STAGE) - commands = parser.add_subparsers(dest="command", required=True) - make = commands.add_parser("bundle") - make.add_argument("files", nargs="+") - web = commands.add_parser("serve") - web.add_argument("--port", type=int, default=8000) - args = parser.parse_args() - if args.command == "bundle": - bundle(args.stage_dir, args.files) - else: - serve(args.stage_dir, args.port) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/tcpagent/BUILD.BAT b/tools/tcpagent/BUILD.BAT new file mode 100644 index 0000000..fa13ec9 --- /dev/null +++ b/tools/tcpagent/BUILD.BAT @@ -0,0 +1,6 @@ +@echo off +set WATCOM=C:\DEVEL\WATCOMC +set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN +set INCLUDE=C:\DEVEL\WATCOMC\H +set EDPATH= +wmake diff --git a/tools/tcpagent/INSTALL.BAT b/tools/tcpagent/INSTALL.BAT new file mode 100644 index 0000000..5532775 --- /dev/null +++ b/tools/tcpagent/INSTALL.BAT @@ -0,0 +1,15 @@ +@echo off +if not exist TCPAGENT.EXE goto fail +copy TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE > nul +if errorlevel 1 goto fail +find "TCPAGENT.EXE" C:\FDAUTO.BAT > nul +if not errorlevel 1 goto done +echo SET MTCPCFG=C:\FREEDOS\MTCP.CFG>>C:\FDAUTO.BAT +echo IF EXIST C:\FREEDOS\BIN\TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE>>C:\FDAUTO.BAT +:done +echo TCPAGENT installed. +goto end +:fail +echo TCPAGENT install failed. +verify other 2>nul +:end diff --git a/tools/tcpagent/Makefile b/tools/tcpagent/Makefile new file mode 100644 index 0000000..ceb4f0f --- /dev/null +++ b/tools/tcpagent/Makefile @@ -0,0 +1,38 @@ +# Place this directory at MTCP\APPS\TCPAGENT in an mTCP checkout. +tcp_c_dir = ..\..\TCPLIB +memory_model = -mc +compile_options = @WPP.RSP +tcpobjs = packet.obj arp.obj eth.obj ip.obj tcp.obj tcpsockm.obj udp.obj utils.obj timer.obj ipasm.obj trace.obj +all : tcpagent.exe +.cpp.obj : + wpp $[* $(compile_options) +tcpagent.obj: tcpagent.cpp + wpp $(compile_options) tcpagent.cpp +packet.obj: + wpp $(tcp_c_dir)\packet.cpp $(compile_options) +arp.obj: + wpp $(tcp_c_dir)\arp.cpp $(compile_options) +eth.obj: + wpp $(tcp_c_dir)\eth.cpp $(compile_options) +ip.obj: + wpp $(tcp_c_dir)\ip.cpp $(compile_options) +tcp.obj: + wpp $(tcp_c_dir)\tcp.cpp $(compile_options) +tcpsockm.obj: + wpp $(tcp_c_dir)\tcpsockm.cpp $(compile_options) +udp.obj: + wpp $(tcp_c_dir)\udp.cpp $(compile_options) +utils.obj: + wpp $(tcp_c_dir)\utils.cpp $(compile_options) +timer.obj: + wpp $(tcp_c_dir)\timer.cpp $(compile_options) +trace.obj: + wpp $(tcp_c_dir)\trace.cpp $(compile_options) +ipasm.obj: + wasm -0 -mc $(tcp_c_dir)\ipasm.asm +tcpagent.exe: tcpagent.obj $(tcpobjs) + wlink system dos option map option eliminate option stack=16384 name $@ file *.obj +clean : .symbolic + del *.obj + del *.map + del tcpagent.exe diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md new file mode 100644 index 0000000..d0d60f0 --- /dev/null +++ b/tools/tcpagent/README.md @@ -0,0 +1,32 @@ +# FreeDOS resident TCP agent + +`TCPAGENT.EXE` is a foreground resident automation process. It uses the mTCP +packet-driver stack and maintains an outbound connection to QEMU's host at +`10.0.2.2:5558`. The host relay keeps the existing Pi tool endpoint on 5555. + +## Build in FreeDOS + +1. Obtain the GPLv3 mTCP source tree (tested with the jhpyle/mTCP 2022 fork). +2. Copy this directory to `MTCP\APPS\TCPAGENT` inside that tree. +3. Set `WATCOM` for Open Watcom and run `BUILD.BAT`. +4. Run `INSTALL.BAT`; it installs the executable and adds startup lines after + the existing packet-driver setup in `C:\FDAUTO.BAT`. + +The build uses mTCP's compact memory model and Open Watcom C++16. The agent is +therefore distributed under GPLv3 when linked with mTCP. + +## Protocol + +Legacy text commands remain for Pi tool compatibility: `PING`, `READ`, +`WRITE`, `LIST`, and `EXEC`. Fast staging uses binary framing: + +- `PUT \n` -> `OK\r\n` +- `GET \n` -> `DATA \r\n` +- `HASH \n` -> `STAT \r\n` + +Use `python tools/dos_stage.py --binary LOCAL=C:\\DOS\\PATH` for verified +staging. Verification reads only the length and FNV-1a hash, avoiding a slow +full-file return transfer. + +Use `.qemu/reset.ps1` to restart QEMU. It waits for an established TCP agent +and a successful PONG rather than sleeping for a fixed boot duration. diff --git a/tools/tcpagent/WPP.RSP b/tools/tcpagent/WPP.RSP new file mode 100644 index 0000000..08000f4 --- /dev/null +++ b/tools/tcpagent/WPP.RSP @@ -0,0 +1,11 @@ +-0 +-mc +-DCFG_H="tcpagent.cfg" +-oh +-os +-s +-zp2 +-zpw +-we +-i=..\..\TCPINC +-i=..\..\INCLUDE diff --git a/tools/tcpagent/tcpagent.cfg b/tools/tcpagent/tcpagent.cfg new file mode 100644 index 0000000..b962291 --- /dev/null +++ b/tools/tcpagent/tcpagent.cfg @@ -0,0 +1,12 @@ +#ifndef CONFIG_H +#define CONFIG_H +#define MTCP_PROGRAM_NAME "tcpagent" +#include "Global.Cfg" +#define COMPILE_ARP +#define IP_FRAGMENTS_ON +#define COMPILE_UDP +#define COMPILE_TCP +#define COMPILE_ICMP +#undef TCP_MAX_SOCKETS +#define TCP_MAX_SOCKETS (1) +#endif diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp new file mode 100644 index 0000000..7e5db7e --- /dev/null +++ b/tools/tcpagent/tcpagent.cpp @@ -0,0 +1,205 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "types.h" +#include "trace.h" +#include "utils.h" +#include "packet.h" +#include "arp.h" +#include "udp.h" +#include "tcp.h" +#include "tcpsockm.h" + +#define LINE_SIZE 9000 +#define CHUNK_SIZE 4096 +#define SERVER_PORT 5558 +#define LOCAL_PORT 2058 +#define RECV_SIZE 12288 + +static char linebuf[LINE_SIZE]; +static unsigned char data[CHUNK_SIZE+1]; +static TcpSocket *socketp; +static volatile uint8_t stop_requested; +static FILE *put_file; +static unsigned long put_remaining; + +void __interrupt __far ctrl_break(void) { stop_requested=1; } +void __interrupt __far ctrl_c(void) { stop_requested=1; } + +static void drive(void) { + PACKET_PROCESS_MULT(5); + Arp::driveArp(); + Tcp::drivePackets(); +} + +static int send_all(const void *buffer, unsigned length) { + const uint8_t *p=(const uint8_t *)buffer; + unsigned sent=0; + int rc; + while(sentsend((uint8_t *)(p+sent),length-sent); + if(rc>0) sent+=(unsigned)rc; + else if(rc<0 || socketp->isRemoteClosed()) return -1; + } + return sent==length ? 0 : -1; +} + +static void write_text(const char *s) { send_all(s,strlen(s)); } +static int hexval(int c) { + if(c>='0'&&c<='9') return c-'0'; + if(c>='A'&&c<='F') return c-'A'+10; + if(c>='a'&&c<='f') return c-'a'+10; + return -1; +} +static int decode_hex(const char *src,unsigned char *dst,int cap) { + int n=0,hi,lo; + while(*src&&src[1]) { + if(n>=cap) return -1; + hi=hexval((unsigned char)src[0]); lo=hexval((unsigned char)src[1]); + if(hi<0||lo<0) return -1; + dst[n++]=(unsigned char)((hi<<4)|lo); src+=2; + } + return *src ? -1 : n; +} +static void write_hex(const unsigned char *src,unsigned count) { + static const char digits[]="0123456789ABCDEF"; + char out[256]; unsigned i,n; + while(count) { + n=count>sizeof(out)/2 ? sizeof(out)/2 : count; + for(i=0;i>4]; out[i*2+1]=digits[src[i]&15]; } + send_all(out,n*2); src+=n; count-=n; + } +} +static void ok_data(const unsigned char *src,unsigned count) { + write_text("OK "); write_hex(src,count); write_text("\r\n"); +} +static void error_text(const char *s) { + write_text("ERR "); write_hex((const unsigned char *)s,strlen(s)); write_text("\r\n"); +} +static int decode_path(const char *hex,char *path,int cap) { + int n=decode_hex(hex,(unsigned char *)path,cap-1); + if(n<0) return 0; path[n]='\0'; return 1; +} +static void command_read(char *args) { + char path[260],*off=strchr(args,' '); FILE *f; long pos; size_t count; int eof; + if(!off){error_text("READ requires path and offset");return;} *off++='\0'; + if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;} + pos=atol(off); f=fopen(path,"rb"); if(!f){error_text("Cannot open file");return;} + if(fseek(f,pos,SEEK_SET)){fclose(f);error_text("Cannot seek file");return;} + count=fread(data,1,CHUNK_SIZE,f); eof=count0) if(send_all(data,count)<0)break; + fclose(f); +} +static void command_hash(char *args) { + char path[260]; FILE *f; size_t count; unsigned i; unsigned long length=0,hash=2166136261UL; + if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;} + f=fopen(path,"rb"); if(!f){error_text("Cannot open file");return;} + while((count=fread(data,1,CHUNK_SIZE,f))>0){length+=(unsigned long)count;for(i=0;i=sizeof(output)) break; memcpy(output+used,entry,len); used+=(unsigned)len; } + rc=_dos_findnext(&found); + } + ok_data((unsigned char *)output,used); +} +static void command_exec(char *args) { + char command[700],shell[800]; const char *tmp="C:\\PIEXEC.TMP"; FILE *f; int n,code; size_t count; + n=decode_hex(args,(unsigned char *)command,sizeof(command)-1); if(n<0){error_text("Invalid command encoding");return;} command[n]='\0'; + sprintf(shell,"COMMAND.COM /C %s > %s",command,tmp); code=system(shell); + f=fopen(tmp,"rb"); count=f?fread(data,1,CHUNK_SIZE,f):0; if(f)fclose(f); remove(tmp); + sprintf(linebuf,"OK %d ",code); write_text(linebuf); write_hex(data,count); write_text("\r\n"); +} +static void process_line(char *line) { + char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd); + if(!strcmp(cmd,"PING"))ok_data((const unsigned char *)"PONG",4); + else if(!strcmp(cmd,"READ"))command_read(args); + else if(!strcmp(cmd,"WRITE"))command_write(args); + else if(!strcmp(cmd,"PUT"))command_put(args); + else if(!strcmp(cmd,"GET"))command_get(args); + else if(!strcmp(cmd,"HASH"))command_hash(args); + else if(!strcmp(cmd,"LIST"))command_list(args); + else if(!strcmp(cmd,"EXEC"))command_exec(args); + else if(!strcmp(cmd,"QUIT")){ok_data((const unsigned char *)"BYE",3);stop_requested=1;} + else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); error_text(message); } +} +static int connect_host(void) { + IpAddr_t host={10,0,2,2}; int8_t rc; + socketp=TcpSocketMgr::getSocket(); if(!socketp)return -1; + socketp->setRecvBuffer(RECV_SIZE); + rc=socketp->connect(LOCAL_PORT,host,SERVER_PORT,10000); + while(rc==0&&!socketp->isConnectComplete()&&!socketp->isRemoteClosed()&&!stop_requested)drive(); + if(socketp->isRemoteClosed()){TcpSocketMgr::freeSocket(socketp);socketp=0;return -1;} + return 0; +} +int main(void) { + int used,rc; uint16_t key; + if(Utils::parseEnv()!=0)return 2; + if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c))return 3; + cprintf("[TCPAGENT] Connecting to 10.0.2.2:%u\r\n",SERVER_PORT); + while(!stop_requested) { + if(connect_host()!=0){unsigned long spins=0;while(spins++<60000UL&&!stop_requested)drive();continue;} + cprintf("[TCPAGENT] Connected. Alt-X stops.\r\n"); write_text("TCPAGENT READY\r\n"); used=0; + while(!stop_requested&&!socketp->isRemoteClosed()) { + drive(); rc=socketp->recv((uint8_t *)data,CHUNK_SIZE); + if(rc<0)break; + for(int i=0;i>8)==45)stop_requested=1;} + } + socketp->close(); TcpSocketMgr::freeSocket(socketp); socketp=0; + } + Utils::endStack(); return 0; +} From 2477777e1a9543616a09544a698e3ade774cf1fd Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:02:37 +0900 Subject: [PATCH 021/184] feat: add RapidOCR QEMU console capture --- .gitignore | 5 + pyproject.toml | 9 + tools/README.md | 26 ++ tools/qemu_ocr.py | 87 +++++++ uv.lock | 627 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 754 insertions(+) create mode 100644 pyproject.toml create mode 100644 tools/README.md create mode 100644 tools/qemu_ocr.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index c9ce8fb..089da50 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,11 @@ .DS_Store Thumbs.db +# Python/uv environment and caches +.venv/ +__pycache__/ +*.py[cod] + # Node/tool caches node_modules/ .npm/ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..6abdccd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,9 @@ +[project] +name = "ferrolang" +version = "0.1.0" +description = "Ferro language compiler and QEMU development automation" +requires-python = ">=3.12" +dependencies = [ + "onnxruntime>=1.28.0", + "rapidocr>=3.9.2", +] diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..9798245 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,26 @@ +# Development tools + +## QEMU console OCR + +Capture the current QEMU VGA screen and print detected console text: + +```powershell +uv run python tools/qemu_ocr.py +``` + +Useful options: + +```powershell +# Machine-readable boxes, confidence scores, and text +uv run python tools/qemu_ocr.py --json + +# OCR an existing screenshot without recapturing +uv run python tools/qemu_ocr.py --image .qemu/qemu-screen.png + +# Save extracted text +uv run python tools/qemu_ocr.py -o .qemu/qemu-screen.txt +``` + +The tool invokes `.qemu/screenshot.ps1`, runs RapidOCR with ONNX Runtime, sorts +recognized lines by screen position, and emits UTF-8 text. Screenshots and OCR +output under `.qemu/` remain ignored build artifacts. diff --git a/tools/qemu_ocr.py b/tools/qemu_ocr.py new file mode 100644 index 0000000..41b3ee0 --- /dev/null +++ b/tools/qemu_ocr.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Capture the QEMU VGA console and print its text with RapidOCR.""" + +from __future__ import annotations + +import argparse +import json +import logging +import subprocess +import sys +from pathlib import Path + +from rapidocr import RapidOCR + +ROOT = Path(__file__).resolve().parent.parent +DEFAULT_IMAGE = ROOT / ".qemu" / "qemu-screen.png" +SCREENSHOT = ROOT / ".qemu" / "screenshot.ps1" + + +def capture() -> Path: + subprocess.run( + [ + "powershell", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(SCREENSHOT), + ], + check=True, + stdout=subprocess.DEVNULL, + stderr=subprocess.PIPE, + text=True, + ) + if not DEFAULT_IMAGE.is_file(): + raise RuntimeError(f"QEMU screenshot was not created: {DEFAULT_IMAGE}") + return DEFAULT_IMAGE + + +def recognize(image: Path, min_score: float) -> list[dict[str, object]]: + logging.disable(logging.INFO) + result = RapidOCR()(image) + rows: list[dict[str, object]] = [] + if result.txts is None or result.boxes is None or result.scores is None: + return rows + for box, text, score in zip(result.boxes, result.txts, result.scores): + if float(score) < min_score: + continue + rows.append( + { + "x": int(min(point[0] for point in box)), + "y": int(min(point[1] for point in box)), + "text": text, + "score": round(float(score), 5), + } + ) + rows.sort(key=lambda row: (int(row["y"]), int(row["x"]))) + return rows + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--image", type=Path, help="OCR an existing image instead of capturing QEMU") + parser.add_argument("--min-score", type=float, default=0.5) + parser.add_argument("--json", action="store_true", help="emit locations and scores as JSON") + parser.add_argument("-o", "--output", type=Path, help="also save output as UTF-8 text") + args = parser.parse_args() + + image = args.image.resolve() if args.image else capture() + rows = recognize(image, args.min_score) + if args.json: + rendered = json.dumps({"image": str(image), "lines": rows}, ensure_ascii=False, indent=2) + else: + rendered = "\n".join(str(row["text"]) for row in rows) + if args.output: + args.output.write_text(rendered + ("\n" if rendered else ""), encoding="utf-8") + if rendered: + print(rendered) + return 0 if rows else 1 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except (OSError, RuntimeError, subprocess.CalledProcessError) as exc: + print(f"qemu-ocr: {exc}", file=sys.stderr) + raise SystemExit(2) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..faf4660 --- /dev/null +++ b/uv.lock @@ -0,0 +1,627 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034 } + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456 }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530 }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200 }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222 }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951 }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801 }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070 }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110 }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712 }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977 }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207 }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562 }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507 }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551 }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700 }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584 }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359 }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464 }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676 }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473 }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156 }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246 }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660 }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354 }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638 }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583 }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038 }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677 }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491 }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196 }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660 }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618 }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362 }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755 }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295 }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856 }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318 }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897 }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848 }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163 }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823 }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458 }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717 }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111 }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128 }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240 }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282 }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597 }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376 }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715 }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848 }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521 }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054 }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580 }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325 }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175 }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123 }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682 }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826 }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861 }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758 }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950 }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329 }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137 }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820 }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504 }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087 }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269 }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766 }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814 }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074 }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476 }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115 }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048 }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997 }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014 }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174 }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361 }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143 }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086 }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231 }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546 }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033 }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045 }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866 }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932 }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320 }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174 }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126 }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441 }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742 }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298 }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500 }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888 }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243 }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871 }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580 }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807 }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083 }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317 }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173 }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960 }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186 }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947 }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909 }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467 }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057 }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930 }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822 }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037 }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097 }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166 }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821 }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529 }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348 }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234 }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917 }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846 }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216 }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764 }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318 }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "colorlog" +version = "6.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239 }, +] + +[[package]] +name = "ferrolang" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "onnxruntime" }, + { name = "rapidocr" }, +] + +[package.metadata] +requires-dist = [ + { name = "onnxruntime", specifier = ">=1.28.0" }, + { name = "rapidocr", specifier = ">=3.9.2" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693 }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109 }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202 }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736 }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696 }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264 }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396 }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044 }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817 }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674 }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131 }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595 }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845 }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880 }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264 }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566 }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995 }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511 }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609 }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204 }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532 }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725 }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180 }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878 }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922 }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168 }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501 }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701 }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065 }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031 }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028 }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627 }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414 }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967 }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874 }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276 }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154 }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909 }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685 }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181 }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085 }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971 }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306 }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274 }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846 }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892 }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309 }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850 }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664 }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749 }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495 }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696 }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324 }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466 }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947 }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331 }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336 }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387 }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096 }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730 }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686 }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727 }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775 }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559 }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103 }, +] + +[[package]] +name = "omegaconf" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502 }, +] + +[[package]] +name = "onnxruntime" +version = "1.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362 }, + { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628 }, + { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257 }, + { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036 }, + { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462 }, + { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759 }, + { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339 }, + { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329 }, + { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033 }, + { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175 }, + { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307 }, + { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954 }, + { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748 }, + { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950 }, + { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924 }, + { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738 }, + { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117 }, + { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518 }, + { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976 }, +] + +[[package]] +name = "opencv-python" +version = "5.0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443 }, + { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755 }, + { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064 }, + { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711 }, + { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576 }, + { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032 }, + { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734 }, + { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345 }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956 }, +] + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969 }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323 }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838 }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830 }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383 }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934 }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684 }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137 }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267 }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684 }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487 }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433 }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889 }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109 }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736 }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129 }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562 }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439 }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287 }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691 }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185 }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736 }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435 }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262 }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344 }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131 }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757 }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962 }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171 }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116 }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209 }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707 }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995 }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503 }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956 }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855 }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642 }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281 }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716 }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125 }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939 }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506 }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063 }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549 }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331 }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370 }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147 }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659 }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439 }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577 }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394 }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375 }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048 }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006 }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509 }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167 }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237 }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047 }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440 }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895 }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384 }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537 }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226 }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847 }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030 }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130 }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945 }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996 }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659 }, +] + +[[package]] +name = "pyclipper" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676 }, + { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458 }, + { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235 }, + { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388 }, + { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169 }, + { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619 }, + { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342 }, + { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839 }, + { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142 }, + { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789 }, + { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817 }, + { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007 }, + { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167 }, + { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966 }, + { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216 }, + { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198 }, + { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951 }, + { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782 }, + { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880 }, + { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706 }, + { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308 }, + { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, +] + +[[package]] +name = "rapidocr" +version = "3.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorlog" }, + { name = "numpy" }, + { name = "omegaconf" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pyclipper" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "shapely" }, + { name = "six" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/ed/0ee9b9281986974be9d2406ae0134c8d7c91d2fc613f16ffda9701eeda6f/rapidocr-3.9.2-py3-none-any.whl", hash = "sha256:04d6b8d151f823d930bd91910555f57bea897c0c44fa6794267b94cf9c1ef9a0", size = 27275208 }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075 }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550 }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556 }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308 }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844 }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842 }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714 }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745 }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861 }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644 }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887 }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931 }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855 }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960 }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851 }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890 }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151 }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130 }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802 }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460 }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223 }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760 }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078 }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178 }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756 }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290 }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463 }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145 }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806 }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803 }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301 }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247 }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019 }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137 }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884 }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320 }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931 }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406 }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511 }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607 }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682 }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184 }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, +] From 7cbbb709ebfe85ed34301264e0628cf3b15a2338 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:19:19 +0900 Subject: [PATCH 022/184] feat: consolidate VM automation in Python daemon --- .pi/extensions/dos-vm.md | 20 --- .pi/extensions/dos-vm.ts | 222 ---------------------------- .qemu/monitor.ps1 | 36 ----- .qemu/reset.ps1 | 68 --------- .qemu/run.ps1 | 37 ----- .qemu/screenshot.ps1 | 26 ---- .qemu/tcp-agent-relay.mjs | 71 --------- pyproject.toml | 10 ++ src/ferrolang_vm/__init__.py | 1 + src/ferrolang_vm/cli.py | 99 +++++++++++++ src/ferrolang_vm/daemon.py | 270 +++++++++++++++++++++++++++++++++++ tools/README.md | 50 +++++-- tools/dos_stage.py | 122 ---------------- tools/qemu_ocr.py | 10 +- tools/tcpagent/README.md | 25 ++-- uv.lock | 2 +- 16 files changed, 434 insertions(+), 635 deletions(-) delete mode 100644 .pi/extensions/dos-vm.md delete mode 100644 .pi/extensions/dos-vm.ts delete mode 100644 .qemu/monitor.ps1 delete mode 100644 .qemu/reset.ps1 delete mode 100644 .qemu/run.ps1 delete mode 100644 .qemu/screenshot.ps1 delete mode 100644 .qemu/tcp-agent-relay.mjs create mode 100644 src/ferrolang_vm/__init__.py create mode 100644 src/ferrolang_vm/cli.py create mode 100644 src/ferrolang_vm/daemon.py delete mode 100644 tools/dos_stage.py diff --git a/.pi/extensions/dos-vm.md b/.pi/extensions/dos-vm.md deleted file mode 100644 index 5893081..0000000 --- a/.pi/extensions/dos-vm.md +++ /dev/null @@ -1,20 +0,0 @@ -# DOS VM tools - -This project-local Pi extension exposes `dos_read`, `dos_write`, `dos_edit`, -`dos_list`, and `dos_exec`. It communicates with `DOSAGENT.EXE` over COM1 -through the serial relay controller port at `127.0.0.1:5555`. - -The agent is not started automatically. From the FreeDOS VGA console, run -`DOSAGENT` (installed in `C:\\FREEDOS\\BIN`, which is on `PATH`) when Pi tool -access is needed. While it runs, press Ctrl+C in the VGA console to stop it and -return to the DOS prompt; Pi tools then cannot connect until it is started -again. QEMU monitor control remains available at `127.0.0.1:4444`. - -Connect PuTTY in Raw mode to `127.0.0.1:5557` for a read-only decoded mirror of -Pi/DOS serial traffic. Port 5556 is reserved for the internal QEMU-to-relay -connection. - -`D:` is QEMU's FAT-directory view of `.qemu\\share`. Treat it as an exchange -volume, not a live-sync mount: host-side changes can be stale or partially -visible to a running VM. Restart QEMU before compiling host-edited files, or -use `dos_write` to place the source on `C:` before compiling. diff --git a/.pi/extensions/dos-vm.ts b/.pi/extensions/dos-vm.ts deleted file mode 100644 index 3721710..0000000 --- a/.pi/extensions/dos-vm.ts +++ /dev/null @@ -1,222 +0,0 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { Type } from "typebox"; -import net from "node:net"; - -const HOST = "127.0.0.1"; -const PORT = 5555; -const MONITOR_PORT = 4444; -const CHUNK_SIZE = 4096; -const MAX_FILE_SIZE = 4 * 1024 * 1024; - -function hexEncode(value: string | Buffer): string { - return Buffer.from(value).toString("hex").toUpperCase(); -} - -function hexDecode(value: string): Buffer { - return Buffer.from(value, "hex"); -} - -function normalizePath(path: string): string { - const result = path.startsWith("@") ? path.slice(1) : path; - if (!/^[A-Za-z]:[\\/]/.test(result)) { - throw new Error(`DOS path must be absolute (for example C:\\SRC\\FILE.C): ${result}`); - } - return result.replaceAll("/", "\\"); -} - -async function request(command: string, signal?: AbortSignal, timeoutMs = 30_000): Promise { - return new Promise((resolve, reject) => { - const socket = net.createConnection({ host: HOST, port: PORT }); - let buffered = ""; - let settled = false; - - const finish = (error?: Error, value?: string) => { - if (settled) return; - settled = true; - clearTimeout(timer); - signal?.removeEventListener("abort", abort); - socket.destroy(); - if (error) reject(error); else resolve(value ?? ""); - }; - const abort = () => finish(new Error("DOS tool call aborted")); - const timer = setTimeout(() => finish(new Error(`DOS agent timed out after ${timeoutMs}ms`)), timeoutMs); - - signal?.addEventListener("abort", abort, { once: true }); - socket.setEncoding("ascii"); - socket.on("connect", () => socket.write(`${command}\r\n`, "ascii")); - socket.on("data", (chunk) => { - buffered += chunk; - const lines = buffered.split(/\r?\n/); - buffered = lines.pop() ?? ""; - for (const line of lines) { - if (line.startsWith("OK ")) return finish(undefined, line.slice(3)); - if (line === "OK") return finish(undefined, ""); - if (line.startsWith("ERR ")) return finish(new Error(hexDecode(line.slice(4)).toString("utf8"))); - } - }); - socket.on("error", (error) => finish(new Error(`Cannot connect to DOS agent at ${HOST}:${PORT}: ${error.message}`))); - socket.on("close", () => { - if (!settled) finish(new Error("DOS serial connection closed before a response")); - }); - }); -} - -async function interruptForegroundCommand(): Promise { - await new Promise((resolve, reject) => { - const socket = net.createConnection({ host: HOST, port: MONITOR_PORT }); - const timer = setTimeout(() => { socket.destroy(); reject(new Error("QEMU monitor did not accept Ctrl+C")); }, 5_000); - socket.on("connect", () => socket.write("sendkey ctrl-c\r\n", "ascii")); - socket.on("data", () => { clearTimeout(timer); socket.destroy(); resolve(); }); - socket.on("error", (error) => { clearTimeout(timer); reject(new Error(`Cannot contact QEMU monitor: ${error.message}`)); }); - }); -} - -async function waitForAgentRecovery(timeoutMs = 10_000): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - try { - if (await request("PING", undefined, 1_000) === "504F4E47") return true; - } catch { /* Command is still unwinding. */ } - await new Promise((resolve) => setTimeout(resolve, 250)); - } - return false; -} - -async function readFile(path: string, signal?: AbortSignal): Promise { - const dosPath = normalizePath(path); - const chunks: Buffer[] = []; - let size = 0; - let offset = 0; - for (;;) { - const response = await request(`READ ${hexEncode(dosPath)} ${offset}`, signal); - const match = /^([01])(?:\s(.*))?$/.exec(response); - if (!match) throw new Error(`Malformed READ response: ${response}`); - const chunk = hexDecode(match[2] ?? ""); - chunks.push(chunk); - size += chunk.length; - if (size > MAX_FILE_SIZE) throw new Error(`DOS file exceeds ${MAX_FILE_SIZE} byte tool limit`); - offset += chunk.length; - if (match[1] === "1") return Buffer.concat(chunks, size); - if (chunk.length === 0) throw new Error("DOS agent returned an empty non-final chunk"); - } -} - -async function writeFile(path: string, content: Buffer, signal?: AbortSignal): Promise { - const dosPath = normalizePath(path); - if (content.length > MAX_FILE_SIZE) throw new Error(`Content exceeds ${MAX_FILE_SIZE} byte tool limit`); - if (content.length === 0) { - await request(`WRITE ${hexEncode(dosPath)} T `, signal); - return; - } - for (let offset = 0; offset < content.length; offset += CHUNK_SIZE) { - const chunk = content.subarray(offset, offset + CHUNK_SIZE); - await request(`WRITE ${hexEncode(dosPath)} ${offset === 0 ? "T" : "A"} ${hexEncode(chunk)}`, signal); - } -} - -function textResult(text: string) { - return { content: [{ type: "text" as const, text }], details: {} }; -} - -export default function (pi: ExtensionAPI) { - pi.registerTool({ - name: "dos_read", - label: "DOS Read", - description: "Read a text file from the running FreeDOS VM over COM1.", - promptSnippet: "Read files inside the running FreeDOS VM", - parameters: Type.Object({ path: Type.String({ description: "Absolute DOS path such as C:\\SRC\\MAIN.C" }) }), - async execute(_id, params, signal) { - const content = await readFile(params.path, signal); - return textResult(content.toString("utf8")); - }, - }); - - pi.registerTool({ - name: "dos_write", - label: "DOS Write", - description: "Create or replace a text file in the running FreeDOS VM over COM1.", - promptSnippet: "Write files inside the running FreeDOS VM", - parameters: Type.Object({ - path: Type.String({ description: "Absolute DOS path" }), - content: Type.String({ description: "Complete UTF-8 text content" }), - }), - async execute(_id, params, signal) { - const content = Buffer.from(params.content, "utf8"); - await writeFile(params.path, content, signal); - return textResult(`Wrote ${content.length} bytes to ${normalizePath(params.path)}`); - }, - }); - - pi.registerTool({ - name: "dos_edit", - label: "DOS Edit", - description: "Replace one uniquely matching text fragment in a file in the running FreeDOS VM.", - promptSnippet: "Make exact text replacements inside FreeDOS files", - parameters: Type.Object({ - path: Type.String({ description: "Absolute DOS path" }), - old_text: Type.String({ description: "Text that must occur exactly once" }), - new_text: Type.String({ description: "Replacement text" }), - }), - async execute(_id, params, signal) { - const original = (await readFile(params.path, signal)).toString("utf8"); - const occurrences = original.split(params.old_text).length - 1; - if (occurrences !== 1) throw new Error(`Expected old_text exactly once, found ${occurrences} occurrences`); - const updated = original.replace(params.old_text, params.new_text); - await writeFile(params.path, Buffer.from(updated, "utf8"), signal); - return textResult(`Edited ${normalizePath(params.path)}`); - }, - }); - - pi.registerTool({ - name: "dos_list", - label: "DOS List", - description: "List a directory in the running FreeDOS VM over COM1.", - promptSnippet: "List directories inside the running FreeDOS VM", - parameters: Type.Object({ path: Type.String({ description: "Absolute DOS directory path" }) }), - async execute(_id, params, signal) { - const response = await request(`LIST ${hexEncode(normalizePath(params.path))}`, signal); - return textResult(hexDecode(response).toString("utf8")); - }, - }); - - pi.registerTool({ - name: "dos_exec", - label: "DOS Exec", - description: "Execute a command in the running FreeDOS VM and return its output.", - promptSnippet: "Run commands inside the running FreeDOS VM", - parameters: Type.Object({ - command: Type.String({ description: "FreeDOS command line" }), - timeout_seconds: Type.Optional(Type.Integer({ minimum: 1, maximum: 120, description: "Abort after this many seconds (default: 30)" })), - }), - async execute(_id, params, signal) { - if (params.command.length > 600) throw new Error("DOS command exceeds 600 characters"); - const timeoutMs = (params.timeout_seconds ?? 30) * 1_000; - let response: string; - try { - response = await request(`EXEC ${hexEncode(params.command)}`, signal, timeoutMs); - } catch (error) { - if (signal?.aborted) throw error; - await interruptForegroundCommand(); - const recovered = await waitForAgentRecovery(); - throw new Error(`DOS command timed out after ${timeoutMs / 1_000}s and was interrupted with Ctrl+C${recovered ? "; agent recovered" : "; agent did not recover"}`); - } - const match = /^(-?\d+)(?:\s(.*))?$/.exec(response); - if (!match) throw new Error(`Malformed EXEC response: ${response}`); - const output = hexDecode(match[2] ?? "").toString("utf8"); - return textResult(`${output}${output.endsWith("\n") || !output ? "" : "\n"}[exit ${match[1]}]`); - }, - }); - - pi.registerTool({ - name: "dos_abort", - label: "DOS Abort", - description: "Send Ctrl+C to interrupt the foreground DOS command without rebooting the VM.", - promptSnippet: "Interrupt a hung DOS command without rebooting the VM", - parameters: Type.Object({}), - async execute() { - await interruptForegroundCommand(); - const recovered = await waitForAgentRecovery(); - return textResult(recovered ? "Sent Ctrl+C; DOS agent recovered." : "Sent Ctrl+C; DOS agent has not responded yet."); - }, - }); -} diff --git a/.qemu/monitor.ps1 b/.qemu/monitor.ps1 deleted file mode 100644 index 6a0322f..0000000 --- a/.qemu/monitor.ps1 +++ /dev/null @@ -1,36 +0,0 @@ -param( - [Parameter(Mandatory = $true, Position = 0)] - [string]$Command -) - -$ErrorActionPreference = 'Stop' -$client = [System.Net.Sockets.TcpClient]::new() - -try { - $client.Connect('127.0.0.1', 4444) - $stream = $client.GetStream() - $stream.ReadTimeout = 1000 - $writer = [System.IO.StreamWriter]::new($stream, [System.Text.Encoding]::ASCII, 1024, $true) - $writer.NewLine = "`n" - $writer.AutoFlush = $true - - Start-Sleep -Milliseconds 100 - while ($stream.DataAvailable) { - $buffer = New-Object byte[] 4096 - [void]$stream.Read($buffer, 0, $buffer.Length) - } - - $writer.WriteLine($Command) - Start-Sleep -Milliseconds 200 - - $result = New-Object System.Text.StringBuilder - while ($stream.DataAvailable) { - $buffer = New-Object byte[] 4096 - $count = $stream.Read($buffer, 0, $buffer.Length) - [void]$result.Append([System.Text.Encoding]::ASCII.GetString($buffer, 0, $count)) - } - $result.ToString().Trim() -} -finally { - $client.Dispose() -} diff --git a/.qemu/reset.ps1 b/.qemu/reset.ps1 deleted file mode 100644 index f2465ac..0000000 --- a/.qemu/reset.ps1 +++ /dev/null @@ -1,68 +0,0 @@ -param( - [int] $ReadyTimeoutSeconds = 45 -) - -$ErrorActionPreference = 'Stop' -$root = Split-Path -Parent $MyInvocation.MyCommand.Path - -function Stop-Listener([int] $Port) { - $listeners = Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue - foreach ($listener in $listeners) { - Stop-Process -Id $listener.OwningProcess -Force -ErrorAction SilentlyContinue - } -} - -# Quit through QEMU's monitor so qcow2/FAT writes are flushed. Forced VM -# termination is deliberately forbidden because it previously lost files. -$monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue -if ($monitor) { - & (Join-Path $root 'monitor.ps1') 'quit' | Out-Null - $deadline = (Get-Date).AddSeconds(10) - do { - Start-Sleep -Milliseconds 250 - $monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue - } while ($monitor -and (Get-Date) -lt $deadline) - if ($monitor) { throw 'QEMU did not quit cleanly; refusing a forced reset.' } -} -Stop-Listener 5555 -Start-Sleep -Milliseconds 500 - -$runner = Join-Path $root 'run.ps1' -Start-Process powershell -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File',$runner) -WorkingDirectory $root -$deadline = (Get-Date).AddSeconds(15) -do { - Start-Sleep -Milliseconds 250 - $monitor = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue -} while (-not $monitor -and (Get-Date) -lt $deadline) -if (-not $monitor) { throw 'QEMU monitor did not start.' } - -# Select the default boot entry. TCPAGENT is started by FDAUTO.BAT after the -# packet driver, so readiness is the agent connection/PONG rather than sleep. -Start-Sleep -Seconds 2 -& (Join-Path $root 'monitor.ps1') 'sendkey ret' | Out-Null -$deadline = (Get-Date).AddSeconds($ReadyTimeoutSeconds) -$ready = $false -do { - Start-Sleep -Milliseconds 500 - $connection = Get-NetTCPConnection -State Established -LocalPort 5558 -ErrorAction SilentlyContinue - if ($connection) { - try { - $client = [Net.Sockets.TcpClient]::new('127.0.0.1',5555) - $stream = $client.GetStream() - $stream.ReadTimeout = 1500 - $bytes = [Text.Encoding]::ASCII.GetBytes("PING`n") - $stream.Write($bytes,0,$bytes.Length) - $buffer = New-Object byte[] 128 - $reply = '' - while ($reply -notmatch "`n") { - $count = $stream.Read($buffer,0,$buffer.Length) - if ($count -le 0) { break } - $reply += [Text.Encoding]::ASCII.GetString($buffer,0,$count) - } - $client.Dispose() - $ready = $reply -match '^OK 504F4E47' - } catch { $ready = $false } - } -} while (-not $ready -and (Get-Date) -lt $deadline) -if (-not $ready) { throw 'TCPAGENT did not become ready.' } -Write-Host 'QEMU reset complete; TCPAGENT answered PONG.' diff --git a/.qemu/run.ps1 b/.qemu/run.ps1 deleted file mode 100644 index eefe499..0000000 --- a/.qemu/run.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -param() - -$ErrorActionPreference = 'Stop' -$qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop -$node = Get-Command node.exe -ErrorAction Stop -$disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$relay = Join-Path $PSScriptRoot 'tcp-agent-relay.mjs' - -if (-not (Test-Path -LiteralPath $disk)) { - throw "Missing $disk. Run .qemu\\setup.ps1, then .qemu\\install.ps1." -} - -$relayListening = Get-NetTCPConnection -State Listen -LocalPort 5558 -ErrorAction SilentlyContinue -if (-not $relayListening) { - Start-Process -FilePath $node.Source -ArgumentList @($relay) -WorkingDirectory $PSScriptRoot -WindowStyle Hidden - $deadline = [DateTime]::UtcNow.AddSeconds(5) - do { - Start-Sleep -Milliseconds 100 - $relayListening = Get-NetTCPConnection -State Listen -LocalPort 5558 -ErrorAction SilentlyContinue - } while (-not $relayListening -and [DateTime]::UtcNow -lt $deadline) - if (-not $relayListening) { throw 'TCP agent relay did not start.' } -} - -$monitorListening = Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue -if ($monitorListening) { - throw 'QEMU monitor port 4444 is already in use. Stop the existing VM before starting another one.' -} - -& $qemu.Source ` - -machine pc,accel=whpx,kernel-irqchip=off,usb=on ` - -smp 1 ` - -m 64 ` - -drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" ` - -nic user,model=ne2k_isa ` - -monitor "tcp:127.0.0.1:4444,server=on,wait=off" ` - -boot order=c ` - -display default diff --git a/.qemu/screenshot.ps1 b/.qemu/screenshot.ps1 deleted file mode 100644 index bd5c93d..0000000 --- a/.qemu/screenshot.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -param( - [string]$Name = 'qemu-screen', - [switch]$KeepPpm -) - -$ErrorActionPreference = 'Stop' -$ffmpeg = Get-Command ffmpeg.exe -ErrorAction Stop - -$target = if ([IO.Path]::IsPathRooted($Name)) { $Name } else { Join-Path $PSScriptRoot $Name } -$extension = [IO.Path]::GetExtension($target) -if ($extension -in @('.png', '.ppm')) { - $target = [IO.Path]::Combine([IO.Path]::GetDirectoryName($target), [IO.Path]::GetFileNameWithoutExtension($target)) -} -$directory = [IO.Path]::GetDirectoryName($target) -if ($directory) { New-Item -ItemType Directory -Force -Path $directory | Out-Null } - -$ppm = "$target.ppm" -$png = "$target.png" -$qemuPath = $ppm.Replace('\', '/') - -& (Join-Path $PSScriptRoot 'monitor.ps1') "screendump $qemuPath" | Out-Null -if (-not (Test-Path -LiteralPath $ppm)) { throw 'QEMU did not create the requested screenshot.' } -& $ffmpeg.Source -y -loglevel error -i $ppm $png -if ($LASTEXITCODE -ne 0) { throw "ffmpeg conversion failed with exit code $LASTEXITCODE." } -if (-not $KeepPpm) { Remove-Item -LiteralPath $ppm -Force } -Get-Item -LiteralPath $png diff --git a/.qemu/tcp-agent-relay.mjs b/.qemu/tcp-agent-relay.mjs deleted file mode 100644 index 2768bc4..0000000 --- a/.qemu/tcp-agent-relay.mjs +++ /dev/null @@ -1,71 +0,0 @@ -import net from "node:net"; - -const HOST = "127.0.0.1"; -const TOOL_PORT = 5555; -const OBSERVER_PORT = 5557; -const TCP_AGENT_PORT = 5558; - -let tcpAgent = null; -let controller = null; -const observers = new Set(); - -function safeWrite(socket, data) { - if (socket && !socket.destroyed && socket.writable) socket.write(data); -} - -function trace(source, data) { - const text = data.toString("ascii"); - const shortened = text.length > 400 ? `${text.slice(0, 400)}…` : text; - for (const observer of observers) safeWrite(observer, `${source} ${shortened}\r\n`); -} - -const tcpAgentServer = net.createServer((socket) => { - if (tcpAgent && !tcpAgent.destroyed) { - socket.end(); - return; - } - tcpAgent = socket; - socket.setNoDelay(true); - socket.on("data", (data) => { - safeWrite(controller, data); - trace("DOS->Pi", data); - }); - socket.on("close", () => { - if (tcpAgent === socket) tcpAgent = null; - }); - socket.on("error", () => {}); -}); - -const toolServer = net.createServer((socket) => { - if (controller && !controller.destroyed) { - socket.end("ERR 544350204147454E5420434F4E54524F4C4C45522042555359\r\n"); - return; - } - if (!tcpAgent || tcpAgent.destroyed) { - socket.end("ERR 544350204147454E54204E4F5420434F4E4E4543544544\r\n"); - return; - } - controller = socket; - socket.setNoDelay(true); - socket.on("data", (data) => { - safeWrite(tcpAgent, data); - trace("Pi->DOS", data); - }); - socket.on("close", () => { - if (controller === socket) controller = null; - }); - socket.on("error", () => {}); -}); - -const observerServer = net.createServer((socket) => { - observers.add(socket); - socket.setNoDelay(true); - socket.write("DOS TCP agent observer (read-only)\r\n"); - socket.on("data", () => {}); - socket.on("close", () => observers.delete(socket)); - socket.on("error", () => observers.delete(socket)); -}); - -tcpAgentServer.listen(TCP_AGENT_PORT, HOST); -toolServer.listen(TOOL_PORT, HOST); -observerServer.listen(OBSERVER_PORT, HOST); diff --git a/pyproject.toml b/pyproject.toml index 6abdccd..2660113 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,3 +7,13 @@ dependencies = [ "onnxruntime>=1.28.0", "rapidocr>=3.9.2", ] + +[project.scripts] +ferro-vm = "ferrolang_vm.cli:main" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/ferrolang_vm"] diff --git a/src/ferrolang_vm/__init__.py b/src/ferrolang_vm/__init__.py new file mode 100644 index 0000000..039a606 --- /dev/null +++ b/src/ferrolang_vm/__init__.py @@ -0,0 +1 @@ +"""Windows-only QEMU and FreeDOS TCP-agent automation.""" diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py new file mode 100644 index 0000000..28aa392 --- /dev/null +++ b/src/ferrolang_vm/cli.py @@ -0,0 +1,99 @@ +"""Command line client for the Windows-only ferro-vm daemon.""" +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +import time +from multiprocessing.connection import Client +from pathlib import Path + +from .daemon import PIPE, ROOT + + +def rpc(payload: dict[str, object], start_daemon: bool = False) -> object: + try: + conn = Client(PIPE, family="AF_PIPE") + except (FileNotFoundError, OSError): + if not start_daemon: + raise RuntimeError("ferro-vm daemon is not running; run `uv run ferro-vm start`") + flags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "DETACHED_PROCESS", 0) + subprocess.Popen([sys.executable, "-m", "ferrolang_vm.daemon"], cwd=ROOT, creationflags=flags, close_fds=True) + deadline = time.monotonic() + 5 + while True: + try: + conn = Client(PIPE, family="AF_PIPE") + break + except (FileNotFoundError, OSError): + if time.monotonic() >= deadline: + raise RuntimeError("ferro-vm daemon did not create its control pipe") + time.sleep(.1) + with conn: + conn.send(payload) + response = conn.recv() + if not response["ok"]: + raise RuntimeError(response["error"]) + return response["result"] + + +def main() -> int: + parser = argparse.ArgumentParser(description="Windows-only QEMU/FreeDOS automation") + commands = parser.add_subparsers(dest="op", required=True) + for name in ("start", "stop", "status", "ping", "screenshot", "ocr"): + commands.add_parser(name) + reset = commands.add_parser("reset") + reset.add_argument("--timeout", type=int, default=45) + execute = commands.add_parser("exec") + execute.add_argument("command") + put = commands.add_parser("put") + put.add_argument("source", type=Path) + put.add_argument("destination") + get = commands.add_parser("get") + get.add_argument("source") + get.add_argument("destination", type=Path) + args = parser.parse_args() + + try: + if args.op == "ocr": + result = rpc({"op": "screenshot"}) + import logging + logging.disable(logging.INFO) + from rapidocr import RapidOCR + recognized = RapidOCR()(result["path"]) + print("\n".join(recognized.txts or ())) + return 0 + if args.op == "reset": + rpc({"op": "stop"}, start_daemon=True) + time.sleep(.5) + rpc({"op": "start"}, start_daemon=True) + # FreeDOS displays its default boot menu before FDAUTO.BAT starts + # TCPAGENT. This is input, not a readiness delay. + time.sleep(2) + rpc({"op": "monitor", "command": "sendkey ret"}) + deadline = time.monotonic() + args.timeout + while time.monotonic() < deadline: + try: + if str(rpc({"op": "ping"})["response"]).startswith("OK 504F4E47"): + print(json.dumps({"reset": True, "agent": "PONG"})) + return 0 + except RuntimeError: + time.sleep(.5) + raise RuntimeError("TCPAGENT did not become ready") + payload: dict[str, object] = {"op": args.op} + if args.op == "exec": payload["command"] = args.command + if args.op == "put": + payload["source"] = str(args.source.resolve()) + payload["destination"] = args.destination + if args.op == "get": + payload["source"] = args.source + payload["destination"] = str(args.destination.resolve()) + print(json.dumps(rpc(payload, start_daemon=args.op == "start"), ensure_ascii=False, indent=2)) + return 0 + except RuntimeError as exc: + print(f"ferro-vm: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py new file mode 100644 index 0000000..84ab4bd --- /dev/null +++ b/src/ferrolang_vm/daemon.py @@ -0,0 +1,270 @@ +"""Long-lived Windows host for the FreeDOS TCP agent. + +The only automation TCP listener is 127.0.0.1:5558, used exclusively by +TCPAGENT.EXE. Local commands use a Windows named pipe. +""" +from __future__ import annotations + +import json +import logging +import os +import select +import shutil +import socket +import subprocess +import sys +import threading +import time +from datetime import datetime, timezone +from multiprocessing.connection import Listener +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +QEMU = ROOT / ".qemu" +PIPE = r"\\.\pipe\ferrolang-vm" +AGENT_ADDRESS = ("127.0.0.1", 5558) +MONITOR_ADDRESS = ("127.0.0.1", 4444) +LOG_PATH = QEMU / "ferro-vm.log" + + +def configure_logging() -> None: + QEMU.mkdir(exist_ok=True) + handler = logging.FileHandler(LOG_PATH, encoding="utf-8") + handler.setFormatter(logging.Formatter("%(asctime)s.%(msecs)03dZ %(levelname)-7s %(message)s", "%Y-%m-%dT%H:%M:%S")) + logging.basicConfig(level=logging.INFO, handlers=[handler]) + logging.Formatter.converter = time.gmtime + + +def log_event(level: int, event: str, **fields: object) -> None: + suffix = " ".join(f"{key}={json.dumps(value, ensure_ascii=False)}" for key, value in fields.items()) + logging.log(level, "%s%s", event, f" {suffix}" if suffix else "") + + +class Host: + def __init__(self) -> None: + self.agent: socket.socket | None = None + self.agent_lock = threading.Lock() + self.agent_ready = threading.Event() + self.qemu: subprocess.Popen[bytes] | None = None + + def accept_agents(self) -> None: + server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server.bind(AGENT_ADDRESS) + server.listen(1) + log_event(logging.INFO, "agent listener ready", address="127.0.0.1:5558") + while True: + sock, peer = server.accept() + sock.settimeout(30) + with self.agent_lock: + if self.agent is not None: + sock.close() + log_event(logging.WARNING, "agent rejected", peer=str(peer), reason="already connected") + continue + self.agent = sock + self.agent_ready.set() + try: + banner = self._read_line(sock).decode("ascii", "replace") + log_event(logging.INFO, "agent connected", peer=f"{peer[0]}:{peer[1]}", banner=banner) + # Request() owns protocol reads. Between requests, peek only + # for EOF so TCPAGENT can reconnect without being rejected. + while self.agent is sock: + if self.agent_lock.acquire(blocking=False): + try: + readable, _, _ = select.select([sock], [], [], .25) + if readable and not sock.recv(1, socket.MSG_PEEK): + break + finally: + self.agent_lock.release() + else: + time.sleep(.05) + finally: + with self.agent_lock: + if self.agent is sock: + self.agent = None + self.agent_ready.clear() + sock.close() + log_event(logging.INFO, "agent disconnected", peer=f"{peer[0]}:{peer[1]}") + + @staticmethod + def _read_line(sock: socket.socket) -> bytes: + out = bytearray() + while not out.endswith(b"\n"): + part = sock.recv(1) + if not part: + raise ConnectionError("TCP agent closed connection") + out.extend(part) + return bytes(out).rstrip(b"\r\n") + + def request(self, command: str, payload: bytes = b"") -> str: + with self.agent_lock: + if self.agent is None: + raise RuntimeError("TCPAGENT is not connected") + sock = self.agent + started = time.monotonic() + try: + sock.sendall(command.encode("ascii") + b"\n" + payload) + response = self._read_line(sock).decode("ascii", "replace") + except OSError as exc: + if self.agent is sock: + self.agent = None + self.agent_ready.clear() + raise RuntimeError(f"TCPAGENT request failed: {exc}") from exc + log_event(logging.INFO, "agent request", command=command.split(" ", 1)[0], response=response[:200], elapsed_ms=round((time.monotonic()-started)*1000)) + if response.startswith("ERR "): + raise RuntimeError(bytes.fromhex(response[4:]).decode("utf-8", "replace")) + return response + + def ping(self) -> dict[str, object]: + return {"response": self.request("PING")} + + def exec(self, command: str) -> dict[str, object]: + log_event(logging.INFO, "exec start", command=command) + response = self.request("EXEC " + command.encode("ascii", "replace").hex().upper()) + fields = response.split(" ", 2) + code = int(fields[1]) if len(fields) > 1 else -1 + output = bytes.fromhex(fields[2]).decode("cp437", "replace") if len(fields) > 2 else "" + for line in output.splitlines(): + log_event(logging.INFO, "dos output", line=line) + log_event(logging.INFO, "exec finish", exit=code) + return {"exit": code, "output": output} + + def put(self, source: str, destination: str) -> dict[str, object]: + data = Path(source).read_bytes() + encoded = destination.encode("ascii").hex().upper() + self.request(f"PUT {encoded} {len(data)}", data) + stat = self.request(f"HASH {encoded}") + log_event(logging.INFO, "put", path=destination, bytes=len(data), stat=stat) + return {"path": destination, "bytes": len(data), "stat": stat} + + def get(self, source: str, destination: str) -> dict[str, object]: + encoded = source.encode("ascii").hex().upper() + with self.agent_lock: + if self.agent is None: + raise RuntimeError("TCPAGENT is not connected") + sock = self.agent + sock.sendall(f"GET {encoded}\n".encode("ascii")) + header = self._read_line(sock).decode("ascii", "strict").split() + if len(header) != 2 or header[0] != "DATA": + raise RuntimeError("GET failed: " + " ".join(header)) + remaining = int(header[1]) + chunks: list[bytes] = [] + while remaining: + chunk = sock.recv(min(65536, remaining)) + if not chunk: + raise RuntimeError("TCPAGENT closed during GET") + chunks.append(chunk) + remaining -= len(chunk) + target = Path(destination) + target.parent.mkdir(parents=True, exist_ok=True) + data = b"".join(chunks) + target.write_bytes(data) + log_event(logging.INFO, "get", path=source, bytes=len(data), destination=str(target)) + return {"path": source, "destination": str(target), "bytes": len(data)} + + @staticmethod + def monitor(command: str) -> str: + with socket.create_connection(MONITOR_ADDRESS, timeout=3) as sock: + sock.settimeout(1) + time.sleep(.1) + try: + sock.recv(4096) + except TimeoutError: + pass + sock.sendall(command.encode("ascii") + b"\n") + time.sleep(.2) + chunks: list[bytes] = [] + while True: + try: + chunk = sock.recv(4096) + except TimeoutError: + break + if not chunk: + break + chunks.append(chunk) + return b"".join(chunks).decode("ascii", "replace").strip() + + def start(self) -> dict[str, object]: + if self.qemu is not None and self.qemu.poll() is None: + return {"started": False, "reason": "already running"} + try: + self.monitor("info status") + return {"started": False, "reason": "already running (external)"} + except OSError: + pass + executable = shutil.which("qemu-system-i386.exe") + disk = QEMU / "freedos.qcow2" + if not executable: + raise RuntimeError("qemu-system-i386.exe is not on PATH") + if not disk.exists(): + raise RuntimeError(f"missing {disk}; run .qemu/setup.ps1 and .qemu/install.ps1") + self.qemu = subprocess.Popen([executable, "-machine", "pc,accel=whpx,kernel-irqchip=off,usb=on", "-smp", "1", "-m", "64", "-drive", f"file={disk},format=qcow2,if=ide,index=0,media=disk", "-nic", "user,model=ne2k_isa", "-monitor", "tcp:127.0.0.1:4444,server=on,wait=off", "-boot", "order=c", "-display", "default"], cwd=QEMU) + log_event(logging.INFO, "qemu started", pid=self.qemu.pid) + return {"started": True, "pid": self.qemu.pid} + + def stop(self) -> dict[str, object]: + try: + self.monitor("quit") + log_event(logging.INFO, "qemu stop requested") + except OSError: + pass + return {"stopped": True} + + def screenshot(self) -> dict[str, object]: + ppm, png = QEMU / "qemu-screen.ppm", QEMU / "qemu-screen.png" + self.monitor("screendump " + str(ppm).replace("\\", "/")) + ffmpeg = shutil.which("ffmpeg.exe") + if not ppm.exists() or not ffmpeg: + raise RuntimeError("screenshot failed or ffmpeg.exe is not on PATH") + subprocess.run([ffmpeg, "-y", "-loglevel", "error", "-i", str(ppm), str(png)], check=True) + ppm.unlink(missing_ok=True) + log_event(logging.INFO, "screenshot", path=str(png)) + return {"path": str(png)} + + def dispatch(self, request: dict[str, object]) -> object: + op = request["op"] + if op == "status": + try: + self.monitor("info status") + running = True + except OSError: + running = False + return {"agent_connected": self.agent_ready.is_set(), "qemu_running": running, "log": str(LOG_PATH)} + if op == "start": return self.start() + if op == "stop": return self.stop() + if op == "ping": return self.ping() + if op == "exec": return self.exec(str(request["command"])) + if op == "put": return self.put(str(request["source"]), str(request["destination"])) + if op == "get": return self.get(str(request["source"]), str(request["destination"])) + if op == "screenshot": return self.screenshot() + if op == "monitor": return {"output": self.monitor(str(request["command"]))} + raise ValueError(f"unknown operation: {op}") + + +def serve_pipe(host: Host) -> None: + listener = Listener(PIPE, family="AF_PIPE") + log_event(logging.INFO, "control pipe ready", pipe=PIPE) + while True: + conn = listener.accept() + try: + request = conn.recv() + try: + conn.send({"ok": True, "result": host.dispatch(request)}) + except Exception as exc: + log_event(logging.ERROR, "control failed", error=str(exc)) + conn.send({"ok": False, "error": str(exc)}) + finally: + conn.close() + + +def main() -> None: + if os.name != "nt": + raise SystemExit("ferro-vm currently supports Windows only") + configure_logging() + host = Host() + threading.Thread(target=host.accept_agents, daemon=True).start() + serve_pipe(host) + + +if __name__ == "__main__": + main() diff --git a/tools/README.md b/tools/README.md index 9798245..2b0b4b8 100644 --- a/tools/README.md +++ b/tools/README.md @@ -1,26 +1,48 @@ # Development tools -## QEMU console OCR +## Host support -Capture the current QEMU VGA screen and print detected console text: +Automation currently supports **Windows 10/11 only**. It requires `uv`, QEMU +with WHPX support, and `ffmpeg.exe` on `PATH`. The Python implementation uses +portable APIs where possible, but other hosts are not supported yet. + +## QEMU and FreeDOS automation + +Start the Python daemon and QEMU: ```powershell -uv run python tools/qemu_ocr.py +uv run ferro-vm start +uv run ferro-vm status ``` -Useful options: +`TCPAGENT.EXE` connects only to `127.0.0.1:5558`. Local commands use the +Windows named pipe `\\.\pipe\ferrolang-vm`; there is no controller or observer +TCP port. Monitor the append-only structured log in another terminal: ```powershell -# Machine-readable boxes, confidence scores, and text -uv run python tools/qemu_ocr.py --json +lnav .qemu/ferro-vm.log +``` -# OCR an existing screenshot without recapturing +Commands: + +```powershell +uv run ferro-vm ping +uv run ferro-vm exec 'dir C:\FEC' +uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' +uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK +uv run ferro-vm screenshot +uv run ferro-vm ocr +uv run ferro-vm stop +``` + +The daemon logs command metadata, DOS output, exit status, transfers, and +agent lifecycle events as UTF-8 lines. It deliberately never logs raw binary +payloads or protocol hex. + +## Standalone OCR + +`tools/qemu_ocr.py` remains available for OCRing an existing image: + +```powershell uv run python tools/qemu_ocr.py --image .qemu/qemu-screen.png - -# Save extracted text -uv run python tools/qemu_ocr.py -o .qemu/qemu-screen.txt ``` - -The tool invokes `.qemu/screenshot.ps1`, runs RapidOCR with ONNX Runtime, sorts -recognized lines by screen position, and emits UTF-8 text. Screenshots and OCR -output under `.qemu/` remain ignored build artifacts. diff --git a/tools/dos_stage.py b/tools/dos_stage.py deleted file mode 100644 index 1218749..0000000 --- a/tools/dos_stage.py +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python3 -"""Stage changed source files into the running FreeDOS VM over DOSAGENT. - -Usage: python tools/dos_stage.py host/file=C:\\FEC\\DEST.FILE [...] -The DOS agent uses a 115200-baud virtual COM1 link and accepts at most 4096 -binary payload bytes per ASCII-hex WRITE request. -""" -import hashlib -import socket -import sys -from pathlib import Path - -HOST = "127.0.0.1" -PORT = 5555 -DEFAULT_CHUNK_SIZE = 4096 - - -def request(line: str) -> str: - with socket.create_connection((HOST, PORT), timeout=30) as sock: - sock.sendall((line + "\n").encode("ascii")) - response = bytearray() - while not response.endswith(b"\n"): - part = sock.recv(65536) - if not part: - break - response.extend(part) - text = response.decode("ascii", "strict").strip() - if text.startswith("ERR "): - raise RuntimeError(bytes.fromhex(text[4:]).decode("utf-8", "replace")) - if not text.startswith("OK"): - raise RuntimeError("unexpected DOSAGENT response: " + text) - return text - - -def recv_line(sock: socket.socket) -> bytes: - line = bytearray() - while not line.endswith(b"\n"): - part = sock.recv(1) - if not part: - raise RuntimeError("TCP agent closed the connection") - line.extend(part) - return bytes(line).strip() - - -def fnv1a(data: bytes) -> int: - value = 2166136261 - for byte in data: - value = ((value ^ byte) * 16777619) & 0xFFFFFFFF - return value - - -def binary_stage(local: Path, remote: str) -> None: - data = local.read_bytes() - path_hex = remote.encode("ascii").hex().upper() - with socket.create_connection((HOST, PORT), timeout=30) as sock: - sock.sendall(("PUT %s %d\n" % (path_hex, len(data))).encode("ascii")) - sock.sendall(data) - response = recv_line(sock) - if not response.startswith(b"OK"): - raise RuntimeError("PUT failed: " + response.decode("ascii", "replace")) - sock.sendall(("HASH %s\n" % path_hex).encode("ascii")) - header = recv_line(sock).decode("ascii", "strict") - fields = header.split() - if len(fields) != 3 or fields[0] != "STAT": - raise RuntimeError("HASH failed: " + header) - remote_length = int(fields[1]) - remote_hash = int(fields[2], 16) - if remote_length != len(data) or remote_hash != fnv1a(data): - raise RuntimeError("verification mismatch for " + remote) - print("staged %s -> %s (%d bytes, sha256 %s, binary TCP)" % ( - local, remote, len(data), hashlib.sha256(data).hexdigest())) - - -def stage(local: Path, remote: str, chunk_size: int) -> None: - data = local.read_bytes() - remote_hex = remote.encode("ascii").hex().upper() - for offset in range(0, max(1, len(data)), chunk_size): - part = data[offset : offset + chunk_size] - mode = "T" if offset == 0 else "A" - request("WRITE %s %s %s" % (remote_hex, mode, part.hex().upper())) - remote_data = bytearray() - offset = 0 - while True: - response = request("READ %s %u" % (remote_hex, offset)) - _, eof, payload = response.split(" ", 2) - piece = bytes.fromhex(payload) - remote_data.extend(piece) - offset += len(piece) - if eof == "1": - break - if bytes(remote_data) != data: - raise RuntimeError("verification mismatch for " + remote) - print("staged %s -> %s (%d bytes, sha256 %s)" % ( - local, remote, len(data), hashlib.sha256(data).hexdigest())) - - -def main() -> int: - args = sys.argv[1:] - chunk_size = DEFAULT_CHUNK_SIZE - binary = False - if args and args[0] == "--binary": - binary = True - args = args[1:] - if len(args) >= 2 and args[0] == "--chunk-size": - chunk_size = int(args[1]) - args = args[2:] - if not args: - print("usage: dos_stage.py [--binary] [--chunk-size N] LOCAL=DOS_PATH [... ]", file=sys.stderr) - return 2 - for spec in args: - if "=" not in spec: - raise SystemExit("missing '=' in " + spec) - local, remote = spec.split("=", 1) - if binary: - binary_stage(Path(local), remote) - else: - stage(Path(local), remote, chunk_size) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tools/qemu_ocr.py b/tools/qemu_ocr.py index 41b3ee0..949380f 100644 --- a/tools/qemu_ocr.py +++ b/tools/qemu_ocr.py @@ -14,19 +14,11 @@ from rapidocr import RapidOCR ROOT = Path(__file__).resolve().parent.parent DEFAULT_IMAGE = ROOT / ".qemu" / "qemu-screen.png" -SCREENSHOT = ROOT / ".qemu" / "screenshot.ps1" def capture() -> Path: subprocess.run( - [ - "powershell", - "-NoProfile", - "-ExecutionPolicy", - "Bypass", - "-File", - str(SCREENSHOT), - ], + ["ferro-vm", "screenshot"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md index d0d60f0..afa3fff 100644 --- a/tools/tcpagent/README.md +++ b/tools/tcpagent/README.md @@ -1,8 +1,13 @@ # FreeDOS resident TCP agent `TCPAGENT.EXE` is a foreground resident automation process. It uses the mTCP -packet-driver stack and maintains an outbound connection to QEMU's host at -`10.0.2.2:5558`. The host relay keeps the existing Pi tool endpoint on 5555. +packet-driver stack and maintains an outbound connection to the QEMU host at +`10.0.2.2:5558`. + +The Windows-only Python `ferro-vm` daemon owns that listener. It logs metadata +and decoded command output to `.qemu/ferro-vm.log`; it does not expose an +observer/controller TCP port or emit binary payloads to the log. Local host +control uses a Windows named pipe. ## Build in FreeDOS @@ -17,16 +22,18 @@ therefore distributed under GPLv3 when linked with mTCP. ## Protocol -Legacy text commands remain for Pi tool compatibility: `PING`, `READ`, -`WRITE`, `LIST`, and `EXEC`. Fast staging uses binary framing: +`PING`, `READ`, `WRITE`, `LIST`, and `EXEC` use text commands. Fast transfer +commands are: - `PUT \n` -> `OK\r\n` - `GET \n` -> `DATA \r\n` - `HASH \n` -> `STAT \r\n` -Use `python tools/dos_stage.py --binary LOCAL=C:\\DOS\\PATH` for verified -staging. Verification reads only the length and FNV-1a hash, avoiding a slow -full-file return transfer. +The host invokes them through: -Use `.qemu/reset.ps1` to restart QEMU. It waits for an established TCP agent -and a successful PONG rather than sleeping for a fixed boot duration. +```powershell +uv run ferro-vm put host-file 'C:\DOS\FILE' +uv run ferro-vm get 'C:\DOS\FILE' host-file +``` + +No DOS-side change is required for the Python host automation. diff --git a/uv.lock b/uv.lock index faf4660..c8f60c6 100644 --- a/uv.lock +++ b/uv.lock @@ -172,7 +172,7 @@ wheels = [ [[package]] name = "ferrolang" version = "0.1.0" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "onnxruntime" }, { name = "rapidocr" }, From a637e591e135da133915de5d2c44700367cf6971 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:22:36 +0900 Subject: [PATCH 023/184] feat: add verified QEMU soft reset --- src/ferrolang_vm/cli.py | 15 ++++++++++----- src/ferrolang_vm/daemon.py | 4 ++++ tools/README.md | 7 +++++-- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py index 28aa392..e749203 100644 --- a/src/ferrolang_vm/cli.py +++ b/src/ferrolang_vm/cli.py @@ -44,6 +44,8 @@ def main() -> int: commands.add_parser(name) reset = commands.add_parser("reset") reset.add_argument("--timeout", type=int, default=45) + soft_reset = commands.add_parser("soft-reset") + soft_reset.add_argument("--timeout", type=int, default=45) execute = commands.add_parser("exec") execute.add_argument("command") put = commands.add_parser("put") @@ -63,10 +65,13 @@ def main() -> int: recognized = RapidOCR()(result["path"]) print("\n".join(recognized.txts or ())) return 0 - if args.op == "reset": - rpc({"op": "stop"}, start_daemon=True) - time.sleep(.5) - rpc({"op": "start"}, start_daemon=True) + if args.op in ("reset", "soft-reset"): + if args.op == "reset": + rpc({"op": "stop"}, start_daemon=True) + time.sleep(.5) + rpc({"op": "start"}, start_daemon=True) + else: + rpc({"op": "soft-reset"}) # FreeDOS displays its default boot menu before FDAUTO.BAT starts # TCPAGENT. This is input, not a readiness delay. time.sleep(2) @@ -75,7 +80,7 @@ def main() -> int: while time.monotonic() < deadline: try: if str(rpc({"op": "ping"})["response"]).startswith("OK 504F4E47"): - print(json.dumps({"reset": True, "agent": "PONG"})) + print(json.dumps({"reset": args.op, "agent": "PONG"})) return 0 except RuntimeError: time.sleep(.5) diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py index 84ab4bd..8c55bcb 100644 --- a/src/ferrolang_vm/daemon.py +++ b/src/ferrolang_vm/daemon.py @@ -232,6 +232,10 @@ class Host: return {"agent_connected": self.agent_ready.is_set(), "qemu_running": running, "log": str(LOG_PATH)} if op == "start": return self.start() if op == "stop": return self.stop() + if op == "soft-reset": + self.monitor("system_reset") + log_event(logging.INFO, "qemu soft reset requested") + return {"reset": "requested"} if op == "ping": return self.ping() if op == "exec": return self.exec(str(request["command"])) if op == "put": return self.put(str(request["source"]), str(request["destination"])) diff --git a/tools/README.md b/tools/README.md index 2b0b4b8..810ebea 100644 --- a/tools/README.md +++ b/tools/README.md @@ -26,6 +26,8 @@ lnav .qemu/ferro-vm.log Commands: ```powershell +uv run ferro-vm reset # clean QEMU quit and restart +uv run ferro-vm soft-reset # QEMU system_reset uv run ferro-vm ping uv run ferro-vm exec 'dir C:\FEC' uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' @@ -35,8 +37,9 @@ uv run ferro-vm ocr uv run ferro-vm stop ``` -The daemon logs command metadata, DOS output, exit status, transfers, and -agent lifecycle events as UTF-8 lines. It deliberately never logs raw binary +Both reset modes wait for FreeDOS to boot, submit the default boot-menu Enter, +and require TCPAGENT `PING`/`PONG` before succeeding. The daemon logs command +metadata, DOS output, exit status, transfers, and agent lifecycle events as UTF-8 lines. It deliberately never logs raw binary payloads or protocol hex. ## Standalone OCR From 5d9cd9029926c813c79239e583460c784f63ffe1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:36:24 +0900 Subject: [PATCH 024/184] fix: use clean VM reset and resilient agent listener --- src/ferrolang_vm/cli.py | 64 +++++++++++++++++++++++++------------ src/ferrolang_vm/daemon.py | 15 +++++---- tools/README.md | 14 +++++--- tools/tcpagent/tcpagent.cpp | 3 +- 4 files changed, 63 insertions(+), 33 deletions(-) diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py index e749203..f268a64 100644 --- a/src/ferrolang_vm/cli.py +++ b/src/ferrolang_vm/cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import json +import shutil import subprocess import sys import time @@ -37,15 +38,41 @@ def rpc(payload: dict[str, object], start_daemon: bool = False) -> object: return response["result"] +def wait_ready(timeout: int) -> bool: + """Wait quietly; do not turn an expected boot gap into error-log spam.""" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + try: + status = rpc({"op": "status"}) + if status["agent_connected"] and str(rpc({"op": "ping"})["response"]).startswith("OK 504F4E47"): + return True + except RuntimeError: + pass + time.sleep(.5) + return False + + +def follow_logs() -> int: + log_path = ROOT / ".qemu" / "ferro-vm.log" + lnav = shutil.which("lnav.exe") or shutil.which("lnav") + if lnav: + return subprocess.run([lnav, str(log_path)]).returncode + print("lnav was not found; following the log with PowerShell.", file=sys.stderr) + return subprocess.run([ + "powershell", "-NoProfile", "-Command", + f"Get-Content -LiteralPath '{log_path}' -Wait", + ]).returncode + + def main() -> int: parser = argparse.ArgumentParser(description="Windows-only QEMU/FreeDOS automation") commands = parser.add_subparsers(dest="op", required=True) - for name in ("start", "stop", "status", "ping", "screenshot", "ocr"): + for name in ("start", "stop", "status", "ping", "screenshot", "ocr", "logs"): commands.add_parser(name) + wait = commands.add_parser("wait-ready") + wait.add_argument("--timeout", type=int, default=45) reset = commands.add_parser("reset") reset.add_argument("--timeout", type=int, default=45) - soft_reset = commands.add_parser("soft-reset") - soft_reset.add_argument("--timeout", type=int, default=45) execute = commands.add_parser("exec") execute.add_argument("command") put = commands.add_parser("put") @@ -57,6 +84,13 @@ def main() -> int: args = parser.parse_args() try: + if args.op == "logs": + return follow_logs() + if args.op == "wait-ready": + if wait_ready(args.timeout): + print(json.dumps({"agent": "PONG"})) + return 0 + raise RuntimeError("TCPAGENT did not become ready") if args.op == "ocr": result = rpc({"op": "screenshot"}) import logging @@ -65,25 +99,15 @@ def main() -> int: recognized = RapidOCR()(result["path"]) print("\n".join(recognized.txts or ())) return 0 - if args.op in ("reset", "soft-reset"): - if args.op == "reset": - rpc({"op": "stop"}, start_daemon=True) - time.sleep(.5) - rpc({"op": "start"}, start_daemon=True) - else: - rpc({"op": "soft-reset"}) - # FreeDOS displays its default boot menu before FDAUTO.BAT starts - # TCPAGENT. This is input, not a readiness delay. + if args.op == "reset": + rpc({"op": "stop"}, start_daemon=True) + time.sleep(.5) + rpc({"op": "start"}, start_daemon=True) time.sleep(2) rpc({"op": "monitor", "command": "sendkey ret"}) - deadline = time.monotonic() + args.timeout - while time.monotonic() < deadline: - try: - if str(rpc({"op": "ping"})["response"]).startswith("OK 504F4E47"): - print(json.dumps({"reset": args.op, "agent": "PONG"})) - return 0 - except RuntimeError: - time.sleep(.5) + if wait_ready(args.timeout): + print(json.dumps({"reset": "complete", "agent": "PONG"})) + return 0 raise RuntimeError("TCPAGENT did not become ready") payload: dict[str, object] = {"op": args.op} if args.op == "exec": payload["command"] = args.command diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py index 8c55bcb..03e9abf 100644 --- a/src/ferrolang_vm/daemon.py +++ b/src/ferrolang_vm/daemon.py @@ -62,10 +62,12 @@ class Host: log_event(logging.WARNING, "agent rejected", peer=str(peer), reason="already connected") continue self.agent = sock - self.agent_ready.set() try: banner = self._read_line(sock).decode("ascii", "replace") - log_event(logging.INFO, "agent connected", peer=f"{peer[0]}:{peer[1]}", banner=banner) + if banner != "TCPAGENT READY": + raise ConnectionError(f"invalid TCPAGENT banner: {banner!r}") + self.agent_ready.set() + log_event(logging.INFO, "agent connected", peer=f"{peer[0]}:{peer[1]}") # Request() owns protocol reads. Between requests, peek only # for EOF so TCPAGENT can reconnect without being rejected. while self.agent is sock: @@ -78,6 +80,11 @@ class Host: self.agent_lock.release() else: time.sleep(.05) + except (ConnectionError, OSError) as exc: + # Reset can close a connection during its banner. This is a + # per-connection event, never a reason to kill the listener. + log_event(logging.WARNING, "agent handshake/connection failed", + peer=f"{peer[0]}:{peer[1]}", error=str(exc)) finally: with self.agent_lock: if self.agent is sock: @@ -232,10 +239,6 @@ class Host: return {"agent_connected": self.agent_ready.is_set(), "qemu_running": running, "log": str(LOG_PATH)} if op == "start": return self.start() if op == "stop": return self.stop() - if op == "soft-reset": - self.monitor("system_reset") - log_event(logging.INFO, "qemu soft reset requested") - return {"reset": "requested"} if op == "ping": return self.ping() if op == "exec": return self.exec(str(request["command"])) if op == "put": return self.put(str(request["source"]), str(request["destination"])) diff --git a/tools/README.md b/tools/README.md index 810ebea..dd535d7 100644 --- a/tools/README.md +++ b/tools/README.md @@ -20,14 +20,14 @@ Windows named pipe `\\.\pipe\ferrolang-vm`; there is no controller or observer TCP port. Monitor the append-only structured log in another terminal: ```powershell -lnav .qemu/ferro-vm.log +uv run ferro-vm logs ``` Commands: ```powershell uv run ferro-vm reset # clean QEMU quit and restart -uv run ferro-vm soft-reset # QEMU system_reset +uv run ferro-vm wait-ready --timeout 45 uv run ferro-vm ping uv run ferro-vm exec 'dir C:\FEC' uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' @@ -37,9 +37,13 @@ uv run ferro-vm ocr uv run ferro-vm stop ``` -Both reset modes wait for FreeDOS to boot, submit the default boot-menu Enter, -and require TCPAGENT `PING`/`PONG` before succeeding. The daemon logs command -metadata, DOS output, exit status, transfers, and agent lifecycle events as UTF-8 lines. It deliberately never logs raw binary +`reset` cleanly quits and restarts QEMU, waits for FreeDOS to boot, submits +the default boot-menu Enter, and requires TCPAGENT `PING`/`PONG`. QEMU +`system_reset` is intentionally unsupported because repeated soft resets leave +the FreeDOS NE2000 packet driver stuck during initialization. `logs` starts +`lnav` when installed and otherwise falls back to PowerShell `Get-Content +-Wait`. The daemon logs command metadata, DOS output, exit status, transfers, +and agent lifecycle events as UTF-8 lines. It deliberately never logs raw binary payloads or protocol hex. ## Standalone OCR diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp index 7e5db7e..f15af39 100644 --- a/tools/tcpagent/tcpagent.cpp +++ b/tools/tcpagent/tcpagent.cpp @@ -176,10 +176,9 @@ int main(void) { int used,rc; uint16_t key; if(Utils::parseEnv()!=0)return 2; if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c))return 3; - cprintf("[TCPAGENT] Connecting to 10.0.2.2:%u\r\n",SERVER_PORT); while(!stop_requested) { if(connect_host()!=0){unsigned long spins=0;while(spins++<60000UL&&!stop_requested)drive();continue;} - cprintf("[TCPAGENT] Connected. Alt-X stops.\r\n"); write_text("TCPAGENT READY\r\n"); used=0; + write_text("TCPAGENT READY\r\n"); used=0; while(!stop_requested&&!socketp->isRemoteClosed()) { drive(); rc=socketp->recv((uint8_t *)data,CHUNK_SIZE); if(rc<0)break; From dc2285b4d4696a960c5e4f83cff238f3261592eb Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:48:47 +0900 Subject: [PATCH 025/184] feat: capture complete DOS command diagnostics --- src/ferrolang_vm/daemon.py | 41 ++++++++++++++++--- tools/tcpagent/README.md | 13 ++++-- tools/tcpagent/tcpagent.cpp | 80 +++++++++++++++++++++++++++++++------ 3 files changed, 113 insertions(+), 21 deletions(-) diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py index 03e9abf..e35bdf2 100644 --- a/src/ferrolang_vm/daemon.py +++ b/src/ferrolang_vm/daemon.py @@ -127,14 +127,43 @@ class Host: def exec(self, command: str) -> dict[str, object]: log_event(logging.INFO, "exec start", command=command) - response = self.request("EXEC " + command.encode("ascii", "replace").hex().upper()) - fields = response.split(" ", 2) - code = int(fields[1]) if len(fields) > 1 else -1 - output = bytes.fromhex(fields[2]).decode("cp437", "replace") if len(fields) > 2 else "" + started = time.monotonic() + with self.agent_lock: + if self.agent is None: + raise RuntimeError("TCPAGENT is not connected") + sock = self.agent + try: + encoded = command.encode("ascii", "replace").hex().upper() + sock.sendall(f"EXEC {encoded}\n".encode("ascii")) + header = self._read_line(sock).decode("ascii", "replace") + fields = header.split() + if len(fields) == 4 and fields[0] == "RESULT": + code, remaining, flags = int(fields[1]), int(fields[2]), int(fields[3]) + chunks: list[bytes] = [] + while remaining: + chunk = sock.recv(min(65536, remaining)) + if not chunk: + raise ConnectionError("TCP agent closed during EXEC result") + chunks.append(chunk) + remaining -= len(chunk) + raw = b"".join(chunks) + elif fields and fields[0] == "OK": + # Compatibility with an installed pre-RESULT agent. + code = int(fields[1]); flags = 1 + raw = bytes.fromhex(fields[2]) if len(fields) > 2 else b"" + else: + raise RuntimeError("malformed EXEC response: " + header) + except OSError as exc: + if self.agent is sock: + self.agent = None + self.agent_ready.clear() + raise RuntimeError(f"TCPAGENT EXEC failed: {exc}") from exc + output = raw.decode("cp437", "replace") for line in output.splitlines(): log_event(logging.INFO, "dos output", line=line) - log_event(logging.INFO, "exec finish", exit=code) - return {"exit": code, "output": output} + log_event(logging.INFO, "exec finish", exit=code, bytes=len(raw), flags=flags, + elapsed_ms=round((time.monotonic()-started)*1000)) + return {"exit": code, "output": output, "bytes": len(raw), "flags": flags} def put(self, source: str, destination: str) -> dict[str, object]: data = Path(source).read_bytes() diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md index afa3fff..2261744 100644 --- a/tools/tcpagent/README.md +++ b/tools/tcpagent/README.md @@ -22,8 +22,12 @@ therefore distributed under GPLv3 when linked with mTCP. ## Protocol -`PING`, `READ`, `WRITE`, `LIST`, and `EXEC` use text commands. Fast transfer -commands are: +`PING`, `READ`, `WRITE`, and `LIST` use text commands. `EXEC` captures both +stdout and stderr at the DOS handle level and returns an untruncated raw body: + +- `EXEC \n` -> `RESULT \r\n` + +Fast transfer commands are: - `PUT \n` -> `OK\r\n` - `GET \n` -> `DATA \r\n` @@ -36,4 +40,7 @@ uv run ferro-vm put host-file 'C:\DOS\FILE' uv run ferro-vm get 'C:\DOS\FILE' host-file ``` -No DOS-side change is required for the Python host automation. +The foreground agent shows timestamped connection, transfer, and command +start/finish lines. It also keeps the same metadata in `C:\TCPAGENT.LOG`, +rotating files larger than 256 KiB to `C:\TCPAGENT.OLD`. Payloads and command +output are never written to that metadata log. diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp index f15af39..2aff406 100644 --- a/tools/tcpagent/tcpagent.cpp +++ b/tools/tcpagent/tcpagent.cpp @@ -1,10 +1,13 @@ #include #include #include +#include #include +#include #include #include #include +#include #include "types.h" #include "trace.h" @@ -27,6 +30,35 @@ static TcpSocket *socketp; static volatile uint8_t stop_requested; static FILE *put_file; static unsigned long put_remaining; +static unsigned long put_started; +static char put_path[260]; + +static unsigned long ticks(void) { + long value=0; + _bios_timeofday(_TIME_GETCLOCK,&value); + return (unsigned long)value; +} +static void elapsed_text(unsigned long started,char *out) { + unsigned long now=ticks(),delta=now>=started?now-started:now+(1573040UL-started); + sprintf(out,"%lu.%02lus",delta/18UL,(delta%18UL)*100UL/18UL); +} +static void log_line(int attr,const char *fmt,...) { + struct dostime_t now; va_list ap; char text[760]; + _dos_gettime(&now); va_start(ap,fmt); vsprintf(text,fmt,ap); va_end(ap); + /* Open Watcom's DOS conio has no textattr(), and ANSI escapes are not + interpreted on the installed FreeDOS console. Keep output clean. */ + (void)attr; + cprintf("%02u:%02u:%02u %s\r\n",now.hour,now.minute,now.second,text); + { FILE *f=fopen("C:\\TCPAGENT.LOG","a"); + if(f){fprintf(f,"%02u:%02u:%02u %s\n",now.hour,now.minute,now.second,text);fclose(f);} } +} +static void init_log(void) { + FILE *f=fopen("C:\\TCPAGENT.LOG","rb"); long size=0; + if(f){fseek(f,0,SEEK_END);size=ftell(f);fclose(f);} + if(size>262144L){remove("C:\\TCPAGENT.OLD");rename("C:\\TCPAGENT.LOG","C:\\TCPAGENT.OLD");} + f=fopen("C:\\TCPAGENT.LOG","a"); + if(f){fputs("--- TCPAGENT start ---\n",f);fclose(f);} +} void __interrupt __far ctrl_break(void) { stop_requested=1; } void __interrupt __far ctrl_c(void) { stop_requested=1; } @@ -110,10 +142,11 @@ static void command_put(char *args) { if(!length_text){error_text("PUT requires path and length");return;} *length_text++='\0'; if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;} - put_remaining=strtoul(length_text,0,10); + put_remaining=strtoul(length_text,0,10); strcpy(put_path,path); put_started=ticks(); + log_line(0x0B,"> PUT %s %luB",path,put_remaining); put_file=fopen(path,"wb"); - if(!put_file){put_remaining=0;error_text("Cannot write file");return;} - if(!put_remaining){fclose(put_file);put_file=0;ok_data((const unsigned char *)"",0);} + if(!put_file){put_remaining=0;log_line(0x0C,"< PUT ERR cannot open");error_text("Cannot write file");return;} + if(!put_remaining){char elapsed[24];fclose(put_file);put_file=0;elapsed_text(put_started,elapsed);log_line(0x0A,"< PUT OK %s",elapsed);ok_data((const unsigned char *)"",0);} } static void command_get(char *args) { char path[260]; FILE *f; long length; size_t count; @@ -144,11 +177,28 @@ static void command_list(char *args) { ok_data((unsigned char *)output,used); } static void command_exec(char *args) { - char command[700],shell[800]; const char *tmp="C:\\PIEXEC.TMP"; FILE *f; int n,code; size_t count; + char command[700],elapsed[24]; const char *tmp="C:\\PIEXEC.TMP"; FILE *f; + int n,code=-1,fd=-1,save1=-1,save2=-1; long length=0; size_t count; unsigned long started; n=decode_hex(args,(unsigned char *)command,sizeof(command)-1); if(n<0){error_text("Invalid command encoding");return;} command[n]='\0'; - sprintf(shell,"COMMAND.COM /C %s > %s",command,tmp); code=system(shell); - f=fopen(tmp,"rb"); count=f?fread(data,1,CHUNK_SIZE,f):0; if(f)fclose(f); remove(tmp); - sprintf(linebuf,"OK %d ",code); write_text(linebuf); write_hex(data,count); write_text("\r\n"); + started=ticks(); log_line(0x0E,"> EXEC %.640s",command); + fflush(stdout); fflush(stderr); + save1=dup(1); save2=dup(2); + fd=open(tmp,O_CREAT|O_TRUNC|O_WRONLY|O_BINARY,S_IREAD|S_IWRITE); + if(save1<0||save2<0||fd<0||dup2(fd,1)<0||dup2(fd,2)<0) { + if(fd>=0)close(fd); + if(save1>=0){dup2(save1,1);close(save1);} + if(save2>=0){dup2(save2,2);close(save2);} + log_line(0x0C,"< EXEC ERR redirect failed"); error_text("Cannot capture command output"); return; + } + close(fd); code=system(command); fflush(stdout); fflush(stderr); + dup2(save1,1); dup2(save2,2); close(save1); close(save2); + f=fopen(tmp,"rb"); + if(f){fseek(f,0,SEEK_END);length=ftell(f);fseek(f,0,SEEK_SET);} + elapsed_text(started,elapsed); + log_line(code?0x0C:0x0A,"< EXEC exit=%d %ldB %s",code,length,elapsed); + sprintf(linebuf,"RESULT %d %ld 0\r\n",code,length); write_text(linebuf); + while(f&&(count=fread(data,1,CHUNK_SIZE,f))>0)if(send_all(data,count)<0)break; + if(f)fclose(f); remove(tmp); } static void process_line(char *line) { char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd); @@ -173,11 +223,15 @@ static int connect_host(void) { return 0; } int main(void) { - int used,rc; uint16_t key; - if(Utils::parseEnv()!=0)return 2; - if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c))return 3; + int used,rc; uint16_t key; unsigned attempts=0; + init_log(); + log_line(0x0B,"* TCPAGENT starting; Alt-X returns to DOS"); + if(Utils::parseEnv()!=0){log_line(0x0C,"* MTCP configuration error");return 2;} + if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c)){log_line(0x0C,"* TCP stack initialization error");return 3;} while(!stop_requested) { - if(connect_host()!=0){unsigned long spins=0;while(spins++<60000UL&&!stop_requested)drive();continue;} + if(!attempts)log_line(0x07,"* connecting 10.0.2.2:%u",SERVER_PORT); + if(connect_host()!=0){unsigned long spins=0;++attempts;if(attempts==1||attempts%10==0)log_line(0x0C,"* connect failed; retry %u",attempts);while(spins++<60000UL&&!stop_requested)drive();continue;} + attempts=0; log_line(0x0A,"* connected; host automation owns console"); write_text("TCPAGENT READY\r\n"); used=0; while(!stop_requested&&!socketp->isRemoteClosed()) { drive(); rc=socketp->recv((uint8_t *)data,CHUNK_SIZE); @@ -190,7 +244,7 @@ int main(void) { if(fwrite(data+i,1,take,put_file)!=(size_t)take){fclose(put_file);put_file=0;put_remaining=0;error_text("Short write");} else { put_remaining-=take; i+=(int)take-1; - if(!put_remaining){fclose(put_file);put_file=0;ok_data((const unsigned char *)"",0);} + if(!put_remaining){char elapsed[24];fclose(put_file);put_file=0;elapsed_text(put_started,elapsed);log_line(0x0A,"< PUT OK %s",elapsed);ok_data((const unsigned char *)"",0);} } } else if(c=='\r'||c=='\n') { if(used){linebuf[used]='\0';process_line(linebuf);used=0;} @@ -199,6 +253,8 @@ int main(void) { if(_bios_keybrd(1)){key=_bios_keybrd(0);if((key&0xff)==3||(key>>8)==45)stop_requested=1;} } socketp->close(); TcpSocketMgr::freeSocket(socketp); socketp=0; + if(!stop_requested)log_line(0x0C,"* link lost; retrying"); } + log_line(0x07,"* stopped; returning to DOS"); Utils::endStack(); return 0; } From 41de8aa2dc038471ba121934b36e69b305f299df Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:54:23 +0900 Subject: [PATCH 026/184] docs: revise spec to v0.1.6 and drop stale handoff MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R8을 파생 반환 규칙으로 교체해 str.trim 계열의 슬라이스 반환을 표현 가능하게 하고, 결과를 지역 변수에 바인딩할 수 있게 한다. R6의 대여 구간을 마지막 사용 지점까지로 좁히고, R10에 전역 대여 금지를 추가해 호출 경계에서 배타성이 깨지던 구멍을 막는다. error.Name 코드 부여를 링크 심볼에서 드라이버의 emit 이전 단계로 옮겨 컴파일타임 상수로 만들고, str을 []u8과 별개 타입으로 분리한다. 문법 결함(struct 멤버의 pub, enum 배리언트 필드, catch/orelse 프로덕션, error_decl, expr 규범)과 CLI 플래그 산재, 문서-구현 드리프트를 함께 정리한다. 변경 사유는 SPEC.AUDIT.md에 기록했다. HANDOFF.md는 기준 SHA와 통신 프로토콜이 모두 낡아 제거한다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT --- HANDOFF.md | 212 -------------------------------------------------- SPEC.AUDIT.md | 116 +++++++++++++++++++++++++++ SPEC.md | 120 +++++++++++++++++++++++----- 3 files changed, 215 insertions(+), 233 deletions(-) delete mode 100644 HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md deleted file mode 100644 index 8de2256..0000000 --- a/HANDOFF.md +++ /dev/null @@ -1,212 +0,0 @@ -# doslang 작업 인수인계 - -작성 시각: 2026-08-16 (Asia/Seoul) - -## 최종 목표와 사용자 지시 - -- `SPEC.md`의 최신 명세대로 M14까지 전부 구현한다. -- DOSBox는 사용하지 않는다. 실행과 권위 있는 검증은 **QEMU FreeDOS 내부**에서만 한다. -- 컴파일러 A와 생성 C 모두 VM 내부 Open Watcom으로 컴파일한다. - - 컴파일러 A / bits16: `WCL` - - bits32 생성 C: `WCL386` -- 호스트 Windows는 소스 편집, diff, Git, 직렬 전송에만 사용한다. -- WSL이나 호스트 C 컴파일러 결과를 정식 검증으로 인정하지 않는다. -- 코딩은 가능한 한 `gpt-5.6-luna` 서브에이전트에 맡기고, 루트 에이전트는 명세 감사와 QEMU 검증을 담당한다. -- VGA 데모처럼 복잡한 멀티모달 수동 검증은 완료 게이트에서 제외한다. -- 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. -- 구현 중 명세 판단이 바뀌면 `SPEC.md`를 즉시 최신화하고 `SPEC.AUDIT.md`에 변경 이유를 누적한다. - -## 현재 완료 상태 - -현재 기준 커밋과 원격 `master`는 둘 다 다음 SHA다. - -```text -53bca214b82c3abf7882ad41fb177cc488e999af -``` - -완료 및 QEMU/Open Watcom 검증된 범위: - -- M1: lexer, parser, AST dump -- M2: 기본 타입 검사와 C 방출, bits16/bits32 경로 -- M3: struct, enum, match, 배열, 슬라이스, str, 경계 검사, 반복 참조 -- M4: `@print`, `@fprint`, `@sprint`, 최소 `io.Writer` - -마지막 완료 커밋: - -```text -53bca21 feat: implement M4 formatting builtins -8e6a409 feat: implement M3 aggregate types and iteration -57b47a5 docs: disambiguate control-flow headers -95de333 docs: disambiguate match scrutinees -3aa7618 docs: clarify char and byte conversions -``` - -M4 최종 검증 증거: - -- VM의 `C:\FEC\BUILD.OK`: `OK` -- VM의 `C:\FEC\TEST.OK`: `OK` -- `C:\FEC\TEST-DOS.BAT`: exit 0 -- 오류 코드 7을 반환하는 C89 Writer harness가 `@fprint`의 정확한 오류 보존과 첫 오류 이후 단축 중단을 실행 검증한다. -- 로컬/원격 SHA 일치까지 확인하고 푸시했다. - -## 최신 명세 - -- `SPEC.md`: v0.1.5 -- `SPEC.AUDIT.md`: 최초 감사 이후 설계 변경 로그 -- 주요 후속 결정: - - `char`와 `u8`은 별개 타입이며 명시적 `as`만 허용한다. - - 제어 흐름 헤더 직후 `{`는 본문 시작이다. 헤더 최상위 구조체 초기화식은 괄호로 구분한다. - -## 현재 워크트리: 미완성 M5 - -M5 작업 도중 인수인계를 위해 Luna를 중단했다. **아래 변경은 미검증·미커밋 상태이므로 버리지 말고 먼저 감사할 것.** - -수정 파일: - -```text -fec/src/check.c -fec/src/emit_c.c -fec/src/emit_c.h -fec/src/types.c -fec/src/types.h -fec/test-dos.bat -fec/tests/run-tests.sh -fec/vm-m1.bat -``` - -새 파일: - -```text -fec/tests/m5/defer.fe -fec/tests/m5/owned.fe -fec/tests/m5/bad-move.fe -fec/tests/m5/bad-destroy.fe -``` - -현재 diff 규모는 약 251 insertions / 6 deletions이며 `git diff --check`는 통과했다. 아직 DOS로 전송하거나 빌드하지 않았다. - -현재 부분 구현에 들어간 것으로 확인된 것: - -- `FE_TYPE_OWNED` -- struct의 `has_drop` 메타데이터 일부 -- 단순 `moved` 비트 기반 이동 후 사용 진단 -- `mem.create` / `mem.destroy` 일부 checker/emitter 분기 -- owned drop 및 block cleanup helper -- return/break/continue 정리 경로를 위한 emitter 코드 일부 -- 정상 종료 defer 역순 방출 일부 - -그러나 M5 완료로 간주하면 안 된다. 첫 Luna 결과는 골격뿐이었고 다음 누락 때문에 반려했다. - -- 자동 drop/free가 모든 경로에서 정확히 한 번 실행되는지 -- `mem.create(T) -> !^T`의 실제 C ABI와 초기화 -- 재대입 전에 기존 owned 값 정리 -- defer와 drop의 선언 위치 기준 역순 병합 -- return / break / continue / try 전파에서 cleanup -- 조건부 이동의 `MaybeMoved` 상태와 런타임 live flag -- struct drop 메서드 및 필드 역순 drop -- 분기/루프 상태 합류 -- 누수와 이중 해제를 세는 런타임 harness -- R1/R3 실패 테스트 최소 수량 - -중단 직전 두 번째 Luna 패스가 cleanup 코드를 더 추가했으므로 위 항목 일부가 코드에 들어갔을 수 있다. 다음 세션은 반드시 `git diff`로 실제 구현을 재감사하고, 테스트가 증명하지 않는 기능은 완료 처리하지 말아야 한다. - -현재는 `own.c/h`가 없고 소유권 로직이 주로 `check.c`/`emit_c.c`에 들어가 있다. 복잡도가 계속 커지면 명세 §11.5대로 `own.c/h`와 cleanup/lower 계층을 분리하는 편이 안전하다. - -## 다음 세션 권장 순서 - -1. `git status --short`, `git diff --check`, M5 diff 전체를 읽는다. -2. Luna 읽기 전용 감사 에이전트로 M5와 R1~R5/§11.5를 대조한다. -3. 기존 Luna 구현 에이전트 또는 새 `gpt-5.6-luna`에게 발견된 누락을 수정시킨다. -4. 최소 다음 테스트를 갖춘다. - - owned 정상 scope 종료 cleanup - - 조기 return cleanup - - return 시 defer 실행 - - 중첩 defer/drop 역순 - - owned 재대입 시 기존 값 cleanup - - 함수 인자 이동 후 사용 실패 - - 조건부 이동 후 사용 실패 및 정확한 drop - - 직접 `.drop()` 호출 실패 - - `mem.destroy` 후 재사용/이중 destroy 실패 - - 할당/해제 카운터로 누수 0, double free 0 -5. 변경 파일과 M5 fixture를 직렬 프로토콜로 VM의 `C:\FEC`에 직접 전송한다. -6. `C:\FEC\TEST-DOS.BAT`를 실행한다. 실패하면 출력/생성 C/정확한 fixture를 좁혀 반복 수정한다. -7. `BUILD.OK`, `TEST.OK`, exit 0을 모두 확인한 뒤에만 M5 커밋 및 `git push origin master`. -8. 이후 M6~M14도 같은 방식으로 진행한다. - -## QEMU와 직렬 에이전트 상태 - -인수인계 작성 시 QEMU는 재시작 없이 살아 있다. - -```text -QEMU PID: 11700 -monitor: 127.0.0.1:4444 -DOS tool/controller: 127.0.0.1:5555 -QEMU serial relay: 127.0.0.1:5556 -observer: 127.0.0.1:5557 -``` - -5555에 ASCII 한 줄 명령을 보내는 프로토콜: - -```text -PING -READ -WRITE T|A -EXEC -``` - -- `PING` 정상 응답: `OK 504F4E47` -- `WRITE`는 안전하게 1024바이트 조각으로 보내면 된다. 첫 조각 `T`, 이후 `A`. -- authoritative workspace는 VM의 `C:\FEC`이다. -- QEMU의 vvfat `D:`는 교환용으로만 취급하고 빌드하지 않는다. 과거 D:에서 빌드하다 QEMU가 rename 처리 오류로 종료된 적이 있다. -- DOS의 8.3 파일명 때문에 긴 fixture는 `BAD-ARI.FE`, `TRY-FPR.FE`처럼 명시적으로 짧은 이름으로 전송해야 한다. - -QEMU monitor helper: - -```powershell -.\.qemu\monitor.ps1 'sendkey ctrl-c' -.\.qemu\screenshot.ps1 -``` - -긴 DOS 배치가 멈추면 QEMU를 재시작하지 말고 먼저 `sendkey ctrl-c`를 사용한다. FreeDOS가 다음 프롬프트를 보이면 monitor로 `y`, `ret`을 보낸다. - -```text -Terminate batch file ... (Yes/No/All)? -``` - -그 뒤 `PING` 복구를 확인한다. - -## 빌드 관련 함정 - -- 컴파일러 A는 16비트 large model로 빌드한다. small model은 메모리 부족이 났다. -- DOS 명령줄 길이 제한 때문에 compiler object link는 `*.obj`를 사용한다. -- 따라서 `fec/build-dos.bat`가 먼저 `C:\FEC\*.obj`를 지워 stale 32-bit test object가 섞이지 않게 한다. -- 생성 C의 보수적 미사용 helper 때문에 M4 Watcom 테스트는 `-wx -wcd=202`를 사용한다. W202만 끄고 다른 경고는 오류로 유지한다. -- 호스트에서 컴파일하지 말 것. 정적 text/diff 검사만 허용한다. -- `.qemu/qemu-screen.png`는 진단용이며 커밋하지 않는다. - -## Git과 저장소 - -```text -origin: https://github.com/sebastianrcnt/doslang.git -branch: master -repository visibility: public -``` - -사용자는 모든 검증 커밋을 항상 원격에 푸시하길 원한다. - -이 인수인계 문서는 별도 문서 커밋으로 푸시하되, 현재 미완성 M5 변경은 그 커밋에 포함하지 않는다. - -## 전체 남은 목표 - -- M5: owned/drop/defer/move — 현재 미완성 -- M6: `&`/`&mut`, R1~R8 전체 borrow checker -- M7: `?T`, `E!T`, try/catch -- M8: unit/import/.fei/분리 컴파일/std 초안 -- M9: 제네릭 및 Ferro `List`/`Map` -- M10: bits16 far/asm/interrupt/shared/atomic/critical (VGA 수동 데모 제외) -- M11: Compiler B를 Ferro로 작성하고 A로 빌드 -- M12: self-host fixpoint `B(B(B)) == B(B)` 후 A 폐기 -- M13: 386 네이티브 백엔드 -- M14: 8086 네이티브 백엔드, Watcom 없이 bits16 빌드 - -M14 전체 요구를 증명하기 전에는 goal을 complete로 표시하지 않는다. diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index 038ee87..95a2cdf 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -38,3 +38,119 @@ - 구현 영향: 제어 흐름 헤더의 최상위 식 파싱에서 구조체 초기화를 금지하되 괄호 안에서는 일반 식 파싱 상태를 복원한다. 단순 식별자 조건과 배열·슬라이스 반복은 본문 `{` 앞에서 정상적으로 종료되어야 한다. + +## 2026-08-16 — v0.1.6 + +M6(borrow checker) 착수 전에 확정해야 하는 소유권·참조 규칙 결정과, M5까지 누적된 +문법·일관성 결함 정리를 함께 반영했다. + +### R8 — 파생 반환 규칙으로 전면 교체 + +- 문제: R4가 `[]T`를 함수 반환 타입에서 금지하고 기존 R8의 예외는 `&T`/`&mut T`만 + 다뤘다. 그 결과 §10이 요구하는 `str.trim`, `str.split_at`, `str.find`를 표현할 + 방법이 없었다. 기존 R8의 "결과를 지역 변수에 바인딩 불가" 제약도 `trim` 계열을 + 무의미하게 만들었다. +- 결정: R8을 파생 반환 규칙으로 교체한다. (a) 참조성 파라미터가 정확히 하나이고 + 반환값이 그것에서 파생될 때, (b) 문자열 리터럴이나 `static`에서 파생될 때 + `&T`/`&mut T`/`[]T`/`str`을 반환할 수 있다. 호출 지점에서 (a)의 결과는 그 인자를 + 대여한 것으로 취급하며, 바인딩 금지 제약은 삭제한다. +- 근거: 표기 없는 lifetime elision이며 정의와 호출 양쪽 모두 함수 하나만 보고 + 검증되므로 §1.2를 깨지 않는다. 바인딩 금지는 대여 추적을 피하려던 제약인데, + own.c가 R6를 위해 같은 상태 기계를 이미 돌리므로 추가 비용이 거의 없다. +- 구현 영향: own.c는 호출 결과에 "인자로부터의 대여" 상태를 전파해야 한다. + check.c는 시그니처만 보고 참조성 파라미터 개수와 가변성 관계를 검증한다. + 참조성 파라미터가 둘 이상이면 참조성 반환을 거부한다. + +### R6 — 대여 구간을 마지막 사용 지점까지로 축소 + +- 문제: 대여가 참조 변수의 스코프 끝까지 유지되고 블록 표현식도 없어서 + `let r = &mut x; r.^ = 1; x += 1;`이 에러였다. 회피 수단은 명시적 `{ }`뿐이며, + M11에서 컴파일러 B를 이 언어로 작성할 때 마찰이 누적된다. +- 결정: 대여 구간을 참조 변수의 마지막 사용 지점까지로 한다. 조건부 흐름에서는 + 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조는 문장 끝까지로 + 유지한다. +- 근거: 함수 지역 liveness 분석이므로 §1.2를 위반하지 않는다. +- 시점: M6 착수 전에 결정해야 한다. 나중에 좁히면 진단 메시지와 `fail/` 기대값을 + 전부 다시 써야 한다. +- 구현 영향: own.c에 역방향 liveness 스캔 한 번을 추가한다. + +### R10 — 전역에 대한 대여 금지 + +- 문제: R5가 참조 대상으로 전역을 허용하므로 `fn f(r: &mut i32) { G = 5; }`를 + `f(&mut G)`로 호출하면 R6의 배타성이 호출 경계에서 깨진다. 호출자는 `&mut G`가 + 배타적이라고 보고, 피호출자는 자기 파라미터가 `G`를 가리키는지 알 수 없다. + 함수 단위 지역 검사로는 원리적으로 검출 불가능하다. +- 결정: `static`(불변)만 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 + 대여 불가이며 직접 읽기/쓰기만 허용한다. `shared var`는 `critical` 안의 직접 + 접근만 허용한다. 전역을 참조로 넘겨야 하면 지역 변수로 복사한다. +- 구현 영향: R5의 대여 대상에서 가변 전역을 제외한다. §11.4에 방출 단계가 + aliasing을 가정하지 않는다는 규정을 추가했다. 이 규칙이 없으면 `&mut T`에 + `restrict`를 붙이거나 M13/M14 네이티브 백엔드에서 noalias를 가정하는 순간 + 불건전해진다. + +### `error.Name` 코드 부여 시점 + +- 문제: 코드를 "최종 링크용 생성 헤더의 심볼"로 참조하도록 규정했는데, 그러면 C에서 + 상수식이 아니므로 §11.4의 `switch` 방출을 쓸 수 없고 에러 `match`가 if-else + 체인으로 떨어진다. +- 검토 후 기각한 대안: 이름 문자열의 u16 해시. 유닛별 독립 계산과 캐시 유지라는 + 장점이 있으나 생일 문제로 이름 약 300개에서 충돌 확률이 50%에 달해 컴파일러 B의 + 에러 이름 규모를 감당하지 못한다. +- 결정: 정렬 기반 번호 부여는 유지하되, 부여 시점을 링크가 아니라 **드라이버의 emit + 이전 단계**로 옮긴다. 코드는 방출 C에서 컴파일타임 정수 상수가 된다. +- 대가: 이름 집합이 바뀌면 방출 `.c`와 오브젝트 캐시가 전부 무효화된다. `.fei`는 + 이름만 기록하므로 무효화되지 않는다. 유닛 단위 `--emit-c`는 `--error-table`로 + 확정 표를 받아야 한다. +- 구현 영향: driver.c가 전체 `.fei`에서 이름을 수집해 코드를 확정한 뒤 emit을 + 시작한다. + +### `str`을 `[]u8`과 별개 타입으로 + +- 문제: §4.2는 `str`을 "`[]u8` 불변 별칭"이라 하고 §4.7은 별칭을 완전 동일 취급이라 + 규정했다. 완전 동일이면 `str`을 통해 쓸 수 있는데, 문자열 리터럴은 읽기 전용 + 저장 영역에 놓이므로 안전성 구멍이다. +- 결정: `str`을 별개의 내장 타입으로 한다. `[]u8` → `str`은 `as str`로 변환 가능 + (가변성 약화이므로 안전), 역방향은 금지. `str` 원소 쓰기는 컴파일 에러. +- 구현 영향: §11.4에 `fe_str`(`const uint8_t*`) 방출 행을 추가했다. types.c는 + `str`을 `[]u8`과 다른 인터닝 엔트리로 다뤄야 한다. + +### `catch` 블록은 값을 만들지 않는다 + +- 문제: §4.6은 catch 블록이 값을 만들 수 있다고 했으나 문법에 블록 표현식이 없다. +- 결정: 블록 표현식을 도입하는 대신 catch 블록에서 값 생성을 금지한다. 블록은 + `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝난다. 값이 필요하면 + 짧은 형태 `expr catch <식>`을 쓴다. +- 근거: §1.5. 블록 표현식은 §13에 `편의` 등급으로 등재했다. + +### 문법 결함 정리 (§6.1) + +- `struct_decl`이 `field* fn_decl*`이라 §4.3 예제의 `pub fn new`가 문법 위반이었고 + §8이 요구하는 필드별 `pub`도 표현 불가였다. `member := ['pub'] (field | fn_decl)` + 으로 교체하면서 필드와 메서드의 순서 강제도 함께 풀었다. +- enum 배리언트 필드가 struct `field`를 재사용해 `pub`을 받을 수 있었다. `vfield`로 + 분리했다. +- `catch`/`orelse`는 §6.2 우선순위 표에 이름만 있고 프로덕션이 없었다. 추가했다. +- `error_decl`이 빈 에러 집합을 허용하고 마지막 쉼표를 강제했다. 최소 1개 + 선택적 + 후행 쉼표로 수정했다. +- §6.2에 `expr` 프로덕션이 없어 `try`, `as`, `@builtin`, 구조체 초기화가 EBNF + 어디에도 나오지 않았다. 우선순위 표가 표현식 문법의 규범임을 명시했다. +- `Some`/`None`이 패턴에 하드코딩돼 있으나 §3 예약어가 아니었다. 패턴 위치 전용 + 문맥 키워드임을 §4.5에 명시했다. + +### 일관성과 문서 정합 + +- §4.4의 `u16` 태그 승격 규정이 §11.4 방출 표의 `uint8_t` 고정과 어긋났다. 표를 + 수정했다. +- CLI 플래그가 §2, §7.4, §8, R3에 흩어져 있었다. §8.1로 통합했다. +- §11.3의 목표 디렉터리에 `own.c`/`lower.c`/`resolve.c`/`generic.c`/`rt/`가 있으나 + 실제로는 없다. 목표 구조는 유지하고, M5까지 `check.c`/`emit_c.c` 통합 상태이며 + M6 착수 시 `own.c/h`를 분리한다는 단서를 달았다. +- §12의 테스트 구조가 실제 `tests/m/`와 달랐다. 두 축을 모두 인정하도록 했다. + R8 변경에 따라 `fail/` 목록의 "참조 반환 바인딩" 항목을 교체하고 R10 전역 대여, + `str` 변환 케이스를 추가했으며 R6·R8 `pass/` 케이스를 신설했다. + +### 삭제 + +- `HANDOFF.md`를 제거했다. 여기에만 있던 빌드 함정(컴파일러 A는 16비트 large model, + 링크는 `*.obj`, M4 Watcom 테스트는 `-wx -wcd=202`, 8.3 파일명 제약, `D:`에서 빌드 + 금지)은 별도 문서로 옮겨야 한다. diff --git a/SPEC.md b/SPEC.md index 1608898..b153468 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.5 +# Ferro 언어 명세 v0.1.6 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -81,7 +81,7 @@ and or not orelse |---|---|---| | `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` | | `[]T` | 슬라이스 (참조성, §5 R4 적용) | `(ptr, len)` | -| `str` | `[]u8` 불변 별칭 | `(ptr, len)` | +| `str` | 불변 바이트 슬라이스. `[]u8`과 **별개 타입** | `(ptr, len)` | | `^T` | 소유 포인터 (힙, 단일 소유자) | 포인터 | | `&T` | 공유 참조 | 포인터 | | `&mut T` | 배타 참조 | 포인터 | @@ -94,6 +94,7 @@ and or not orelse - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. - 소유 슬라이스가 필요하면 `^[]T`(길이 있는 힙 버퍼)를 쓴다. `mem.alloc_slice(T, n)`가 반환. +- `str`은 별칭이 아니라 별개의 내장 타입이다. `[]u8`에서 `str`로는 `as str`로 변환할 수 있다(가변성 약화이므로 안전). `str`에서 `[]u8`로의 변환은 불변성을 깨므로 금지한다. `str`의 원소는 읽기만 가능하며 `s[i] = v`는 컴파일 에러다. 문자열 리터럴은 읽기 전용 저장 영역에 놓이므로 이 구분이 필요하다. ### 4.3 구조체 @@ -141,6 +142,7 @@ let v = p orelse default_node; // null이면 우변 - `?T`에서 T가 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용(크기 증가 없음). - 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. +- `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. ### 4.6 에러 @@ -162,18 +164,23 @@ fn read_all(path: str) -> IoError!^[]u8 { - `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. - `E!T` 함수만 `try`/`catch` 사용 가능. - `try e`: 에러면 현재 함수에서 즉시 반환(현재 함수도 에러 유니온을 반환해야 함). -- `e catch |x| { ... }`: 블록은 값을 만들거나 `return`/`break`로 탈출. -- `e catch default_value`: 짧은 형태. +- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) +- `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. - 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. -최종 빌드에서 모든 유닛의 이름을 합치고 중복을 제거한 뒤 이름의 바이트순으로 -정렬하여 1부터 안정적인 `u16` 코드를 부여한다. 따라서 서로 다른 유닛의 -`error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이다. -코드는 최종 링크용 생성 헤더의 심볼로 참조하므로 분리 컴파일에서도 일관된다. +최종 빌드에서 **드라이버가 emit 단계 이전에** 모든 유닛의 `.fei`에서 사용된 이름을 +합치고 중복을 제거한 뒤 이름의 바이트순으로 정렬하여 1부터 `u16` 코드를 부여한다. +따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 +결과가 결정적이며, 방출되는 C에서 **컴파일타임 정수 상수**이므로 `switch` case +라벨로 쓸 수 있다. +이름 집합이 바뀌면 코드가 재배치되므로 방출된 `.c`와 오브젝트 캐시는 전부 +무효화된다. `.fei` 자체는 코드가 아니라 이름만 기록하므로 무효화되지 않는다. +유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 +확정된 표를 받아야 하며, 없으면 컴파일 에러다. 이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal 타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 @@ -185,6 +192,7 @@ fn read_all(path: str) -> IoError!^[]u8 { ### 4.7 타입 동등성 이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. 별칭은 `const Alias = Type;`으로 만들며 완전 동일 취급. +`str`은 이 의미의 별칭이 아니라 `[]u8`과 구별되는 별개의 내장 타입이다(§4.2). --- @@ -212,19 +220,43 @@ fn read_all(path: str) -> IoError!^[]u8 { 이 한 줄이 라이프타임 표기 전체를 불필요하게 만든다. -**R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 전역이므로 스코프 중첩 확인만으로 검사된다. +**R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 `static` 전역이므로 스코프 중첩 확인만으로 검사된다. 가변 전역에 대한 대여는 R10이 금지한다. -**R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. 참조의 생존 구간은 **참조 변수의 스코프 끝까지**(NLL 아님). 임시 참조(`f(&x)`)는 그 문장 끝까지. +**R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. + +참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. + +이 판정은 함수 지역 liveness 분석이며 함수 밖 정보를 쓰지 않으므로 §1.2를 위반하지 않는다. + +```fe +var x: i32 = 0; +let r = &mut x; +r.^ = 1; // r의 마지막 사용 +x += 1; // OK — 여기서 r의 대여는 이미 끝났다 +``` **R7 (참조 무효화).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. -**R8 (반환값 예외).** 메서드는 `-> &T` / `-> &mut T`를 반환할 수 있다. 단, 첫 파라미터가 `&Self`/`&mut Self`이고 반환 참조는 그 self에서 파생된 것이어야 하며(컴파일러가 확인), **호출 결과는 그 문장 안에서만 사용 가능**하고 지역 변수에 바인딩할 수 없다. +**R8 (파생 반환).** 함수는 다음 두 경우에 한해 `&T`, `&mut T`, `[]T`, `str`을 반환할 수 있다. + +**(a) 파라미터 파생.** 파라미터 중 참조성 타입(`&T`, `&mut T`, `[]T`, `str`)이 **정확히 하나**이고, 반환값이 그 파라미터에서 파생된 것임을 컴파일러가 함수 본문만 보고 확인할 수 있을 때. 파생이란 슬라이싱, 인덱싱, 필드 접근, `&`/`&mut` 취함, 그리고 다른 R8(a) 함수 호출의 연쇄를 말한다. 반환의 가변성은 원본 이하여야 한다(`&T`에서 `&mut T`를 만들 수 없다). 메서드의 `self`도 이 "하나"에 해당한다. + +**(b) 정적 파생.** 반환값이 문자열 리터럴 또는 `static` 선언에서 파생된 경우. 이때는 참조성 파라미터가 없어도 된다. + +호출 지점에서 R8(a)의 결과는 **그 인자를 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 인자 원본에 R6·R7이 그대로 적용된다. R8(b)의 결과는 대여를 만들지 않는다. ```fe -list.at(0).x = 5; // OK -let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 +pub fn trim(s: str) -> str { ... } // R8(a) +let t = str.trim(line); // OK. line은 t의 대여 구간 동안 잠긴다 + +list.at(0).x = 5; // OK +let r = list.at(0); // OK. list를 &mut로 대여 +list.push(1); // 에러: r이 list를 대여 중 (R6) + +pub fn name() -> str { return "main"; } // R8(b) ``` -장수하는 접근이 필요하면 인덱스(`usize`)나 핸들을 쓴다. + +참조성 파라미터가 둘 이상인 함수는 어느 쪽에서 파생되었는지가 시그니처만으로 결정되지 않으므로 참조성 반환을 할 수 없다. 그런 함수가 필요하면 인덱스(`usize`)나 핸들을 반환한다. **R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `@as_far_fn`, `@call_far`, `asm`, `*_unchecked` 함수. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. @@ -233,6 +265,15 @@ let r = list.at(0); // 에러: 반환 참조를 바인딩 불가 - `interrupt fn` 안에서는 `shared var`에 직접 접근할 수 있다. 일반 함수와 `interrupt_safe fn`은 호출 문맥을 알 수 없으므로 명시적 `critical` 밖의 비원자 공유 접근이 금지된다. - `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/포인터 스칼라에만 허용한다. 메인 흐름의 단일 읽기·쓰기에는 컴파일러가 필요한 최소 임계 구역을 생성한다. 복합 read-modify-write는 여전히 명시적 `critical {}`이 필요하다. 지원되지 않는 크기나 타입은 컴파일 에러다. - `interrupt fn`은 `interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점, `critical` 및 안전하지 않은 함수 호출을 사용할 수 없으며 컴파일러가 함수 본문만 보고 검증한다. 이 효과는 `.fei` 시그니처에 기록한다. +- 전역에 대한 대여는 다음으로 제한한다. `static`(불변)은 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 대여할 수 없으며 직접 읽기와 쓰기만 허용한다. `shared var`는 `critical` 안에서의 직접 접근만 허용하고 대여할 수 없다. 전역 값을 참조로 넘겨야 하면 지역 변수로 복사한 뒤 대여한다. +- 이 제한의 근거는 R6다. 전역에 대한 대여가 살아 있는 동안 호출된 다른 함수가 같은 전역에 직접 접근할 수 있고, 그것은 함수 단위 지역 검사로 검출할 수 없다. 아래는 이 제한이 없으면 통과해 버리는 예다. + +```fe +var G: i32 = 0; +fn f(r: &mut i32) { G = 5; } // r과 G가 같은 곳을 가리키는지 f는 알 수 없다 +fn g() { f(&mut G); } // 제한이 없으면 g의 지역 검사는 통과한다 +``` + - 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1.2에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. **R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. @@ -253,11 +294,13 @@ decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type -struct_decl := ['packed'] 'struct' ident '{' field* fn_decl* '}' +struct_decl := ['packed'] 'struct' ident '{' member* '}' +member := ['pub'] (field | fn_decl) field := ident ':' type ',' enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' -variant := ident | ident '(' type ')' | ident '{' field* '}' -error_decl := 'error' ident '{' (ident '=' int ',')* '}' +variant := ident | ident '(' type ')' | ident '{' vfield* '}' +vfield := ident ':' type ',' +error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}' const_decl := 'const' ident [':' type] '=' expr ';' global_decl := 'static' ident ':' type '=' expr ';' | 'var' ident ':' type '=' expr ';' @@ -298,10 +341,20 @@ type := ident ['.' ident] | '[' expr ']' type | '[' ']' type | 'fn' '(' [type (',' type)*] ')' ['->' type] | ident '(' type (',' type)* ')' // 제네릭 인스턴스 + +catch_expr := expr 'catch' ['|' ident '|'] (expr | block) +orelse_expr := expr 'orelse' expr ``` +`member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 +섞어 쓸 수 있다. `catch`의 블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다. + ### 6.2 표현식 우선순위 (낮음 → 높음) +표현식 문법은 다음 우선순위 표가 규범이다. 각 단계는 명시가 없으면 좌결합 이항 +연산으로 전개하며, 단항·후위·기본 단계에 나열된 형태가 그대로 프로덕션이 된다. +`catch`와 `orelse`의 구체적 형태는 §6.1을 따른다. + ``` 1 orelse, catch 2 or @@ -499,6 +552,21 @@ fec main.fe --emit-c -o out/ # 트랜스파일 결과만 fec --dump-ast main.fe ``` +### 8.1 CLI 플래그 (전체) + +| 플래그 | 의미 | 규정 | +|---|---|---| +| `--target=bits16\|bits32` | 타깃 선택 | §2 | +| `--model=small\|large` | `bits16` 메모리 모델 | §2 | +| `-o <경로>` | 출력 파일 또는 디렉터리 | §8 | +| `-I <디렉터리>` | 유닛 검색 경로 추가 | §8 | +| `--emit-c` | 트랜스파일 결과만 생성 | §8 | +| `--dump-ast` | AST 덤프 | §8 | +| `--no-checks` | 경계·오버플로·`.?` 검사 제거 | §7.4 | +| `--strip-error-names` | 실행 파일에서 에러 이름 문자열 제거 | §4.6 | +| `--error-table=<파일>` | 유닛 단위 `--emit-c`용 확정 에러 코드 표 | §4.6 | +| `--deny-recursive-drop` | 재귀 drop 경고를 에러로 승격 | §5 R3 | + --- ## 9. 제네릭 @@ -602,12 +670,16 @@ fec/ lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개. emit_c.c/h LIR → C. §11.4 규칙. generic.c/h 인스턴스 캐시, 토큰 재파싱. - driver.c CLI, 유닛 의존 순서, .fei 캐시, 외부 C 컴파일러 호출. + driver.c CLI, 유닛 의존 순서, .fei 캐시, 에러 이름 표 수집과 코드 부여(§4.6), + 외부 C 컴파일러 호출. rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim std/ 표준 라이브러리 (.fe) tests/ §12 ``` +위는 목표 구조다. M5까지는 이름 해석·소유권·lower가 `check.c`/`emit_c.c`에 통합되어 +있다. R1~R8 전체를 다루는 M6 착수 시점에 `own.c/h`를 분리한다. + ### 11.4 C 방출 규칙 | Ferro | C | @@ -621,11 +693,12 @@ fec/ | `far X` | `__far X` (Watcom/Borland), bits32는 무시 | | `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) | | `[]T` | `typedef struct { T* p; fe_usize n; } fe_slice_T;` | +| `str` | `typedef struct { const uint8_t* p; fe_usize n; } fe_str;` | | `?T` (포인터류) | 원래 포인터, null 사용 | | `?T` (그 외) | `struct { unsigned char has; T v; }` | | `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | | struct | `struct fe__` | -| enum | `struct { uint8_t tag; union { ... } u; }` | +| enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` | | 함수 | `fe__`, 메서드는 `fe___` | | 제네릭 인스턴스 | `fe____<타입인자맹글>` | @@ -639,6 +712,8 @@ fec/ - **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. - **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. - **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. `@print`는 각 호출의 오류 코드를 명시적으로 버리고 `void`가 되며, `@fprint`는 첫 오류를 전파하고, `@sprint`는 남은 버퍼 길이를 추적해 잘라 쓴 뒤 실제 길이를 반환한다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. +- **참조와 aliasing**: `&T` → `const T*` 방출은 aliasing 가정을 하지 않는다. `&mut T`에도 `restrict`를 붙이지 않으며, M13/M14 네이티브 백엔드도 noalias를 가정하지 않는다. R6의 배타성은 R10의 전역 대여 금지가 함께 성립할 때만 프로그램 전체에서 유지되므로, 방출 단계에서 이를 최적화 근거로 쓰지 않는다. +- **에러 코드**: `error.Name`의 `u16` 코드는 드라이버가 emit 전에 확정한 정수 리터럴로 방출한다(§4.6). 따라서 에러 값에 대한 `match`도 `switch`로 방출할 수 있다. - **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. - **임계 구역**: bits16의 `critical`은 진입 시 FLAGS를 저장한 뒤 `cli`하고 모든 이탈 경로에서 저장한 FLAGS를 복원한다. `shared atomic var`의 단일 접근도 같은 보존형 시퀀스를 사용하며 무조건 `sti`하지 않는다. - **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. @@ -693,15 +768,17 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive ``` tests/ + m/ 마일스톤별 fixture. bad-*.fe는 fail 규약을 따른다 pass/*.fe + *.expected 컴파일→실행→stdout 비교 fail/*.fe 첫 줄 "// ERROR::<메시지 일부>" run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교 boot/ A/B 출력 비교, fixpoint 검증 ``` -- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조 반환 바인딩), R9(unsafe 밖 raw 역참조), match 완전성, 암묵 변환, 타입 불일치. +- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조성 파라미터 2개 이상에서 참조성 반환, 가변성 승격, 파생되지 않은 반환), R9(unsafe 밖 raw 역참조), R10(전역 `var` 대여), match 완전성, 암묵 변환, 타입 불일치, `str`을 `[]u8`로 변환, `str` 원소 쓰기. - 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`, `try @print(...)`(void). - 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print`의 오류 삼킴, `@sprint` 반환 길이/잘림 검증, `@fprint`를 `io.buf_writer`로 호출. +- R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `str.trim` 형태의 슬라이스 반환 연쇄. - `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 `core.Error` 등록, 결정적 코드, `--strip-error-names`, `fmt.write_error`를 각각 pass/fail로 검증한다. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. @@ -727,7 +804,8 @@ tests/ | 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | | `inline fn` | 편의 | — | C 방출 시 `static inline` | | `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | -| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), 인덱스 핸들 | +| 블록 표현식 | 편의 | 값을 만드는 블록이 없으면 `catch`가 짧은 형태로 충분하고, 문법 표면이 작아진다 (§4.6) | `catch <식>`, `return`으로 탈출 | +| 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), R8 파생 반환, 인덱스 핸들 | | 선점형 스레드 | 구조적 불가 | DOS 기본 실행 모델에 없고 함수 단위 소유권 모델을 넘어서는 동기화가 필요 | — | | 매크로 / 전처리기 | **영구** | 도구 지원과 컴파일 속도 파괴 | `const`, `comptime if`, 제네릭, `@print` | | 예외 | 영구 | 언와인딩 기반 시설 없음, 숨은 비용 | 에러 유니온 | From f88559c6355b7333531915d6b0cd7b10c64f75d9 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:54:41 +0900 Subject: [PATCH 027/184] wip: extend M5 ownership tests and cleanup emission MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 조건부 이동, 이중 destroy, 직접 drop 호출에 대한 실패 fixture를 추가하고 runtime harness와 test-dos.bat를 그에 맞춰 갱신한다. M5는 아직 완료가 아니다. 모든 경로에서 정확히 1회 cleanup, defer/drop의 선언 역순 병합, try 전파 경로 cleanup, MaybeMoved 런타임 live flag, struct drop과 필드 역순 drop, 분기/루프 상태 합류, 누수/이중해제 카운터 harness가 남아 있다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT --- fec/src/check.c | 5 +++- fec/src/emit_c.c | 21 ++++++++++++++ fec/src/emit_c.h | 1 + fec/test-dos.bat | 50 ++++++++++++++++++--------------- fec/tests/m5/bad-conditional.fe | 8 ++++++ fec/tests/m5/bad-double.fe | 6 ++++ fec/tests/m5/bad-drop.fe | 11 ++++++++ fec/tests/m5/runtime.c | 43 ++++++++++++++++++++++++++++ fec/tests/m5/runtime.fe | 20 +++++++++---- 9 files changed, 136 insertions(+), 29 deletions(-) create mode 100644 fec/tests/m5/bad-conditional.fe create mode 100644 fec/tests/m5/bad-double.fe create mode 100644 fec/tests/m5/bad-drop.fe diff --git a/fec/src/check.c b/fec/src/check.c index 28d95d9..912936c 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -79,7 +79,10 @@ static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) if (sym->decl) sym->decl->flags |= 0x200U; } else { sym->moved=1; - if (sym->decl) sym->decl->flags |= 0x100U; + /* Mark this consuming expression, not the declaration. Branches + may move conditionally; the declaration's runtime live flag + must remain available to guard cleanup on the other path. */ + n->flags |= 0x100U; } } } diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index ecba29f..102a2c5 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -932,9 +932,17 @@ static void emit_cleanup_to(FeEmitter *e, unsigned floor) for (i=e->block_depth; i>floor; --i) emit_cleanup_block(e,e->block_stack[i-1]); } +static void emit_param_cleanup(FeEmitter *e) +{ + FeNode *p; + if (!e->current_fn || !e->current_fn->a) return; + for (p=e->current_fn->a->children; p; p=p->next) emit_value_drop(e,p); +} + static void emit_cleanup_all(FeEmitter *e) { emit_cleanup_to(e,0); + emit_param_cleanup(e); } static void emit_error_return(FeEmitter *e, const char *error_expr) @@ -974,6 +982,14 @@ static void emit_block(FeEmitter *e, FeNode *n) for (x = n->children; x; x = x->next) if (x->kind == FE_N_LET || x->kind == FE_N_VAR || x->kind == FE_N_CONST) emit_decl(e, x); + if (e->current_fn && e->current_fn->c==n && e->current_fn->a) { + FeNode *param; + for (param=e->current_fn->a->children; param; param=param->next) + if (param->sem_type && param->sem_type->kind==FE_TYPE_OWNED) { + pad(e); fputs("unsigned char fe_live_",e->out); + fputs(cname(param,"owned"),e->out); fputs("=1;\n",e->out); + } + } if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { pad(e); fputs(fe_type_c_name(e->current_ret,e->pointer_bits),e->out); fputs(" fe_return_value;\n",e->out); @@ -988,6 +1004,7 @@ static void emit_block(FeEmitter *e, FeNode *n) } --e->indent; emit_cleanup_block(e,n); + if (e->current_fn && e->current_fn->c==n) emit_param_cleanup(e); if (e->block_depth) --e->block_depth; if (e->fallthrough_block==n) { pad(e); @@ -1246,7 +1263,9 @@ static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) if (prototype) fputs(";\n", e->out); else { FeType *old_ret=e->current_ret; + FeNode *old_fn=e->current_fn; e->current_ret=fn->sem_type; + e->current_fn=fn; fputs(" ", e->out); if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && fn->sem_type->error_value && @@ -1254,6 +1273,7 @@ static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) e->fallthrough_block=fn->c; emit_block(e, fn->c); e->current_ret=old_ret; + e->current_fn=old_fn; fputc('\n', e->out); } } @@ -1285,6 +1305,7 @@ void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, e->block_depth = 0; e->loop_depth = 0; e->current_ret = 0; + e->current_fn = 0; } void fe_emit_c_program(FeEmitter *e) diff --git a/fec/src/emit_c.h b/fec/src/emit_c.h index 93a9bca..10634a8 100644 --- a/fec/src/emit_c.h +++ b/fec/src/emit_c.h @@ -17,6 +17,7 @@ typedef struct FeEmitter { unsigned loop_floor[16]; unsigned loop_depth; FeType *current_ret; + FeNode *current_fn; } FeEmitter; void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, diff --git a/fec/test-dos.bat b/fec/test-dos.bat index 5418842..70026fb 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -15,9 +15,9 @@ fec.exe --dump-ast TESTS\PASS\BASIC.FE > nul if errorlevel 1 goto test_fail fec.exe --dump-ast TESTS\PASS\LITERALS.FE > nul if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\PASS\KEYWOR.FE > nul +fec.exe --dump-ast TESTS\PASS\KEYWORDS-AND-BUILTINS.FE > nul if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\PASS\V012-F.FE > nul +fec.exe --dump-ast TESTS\PASS\V012-FORMS.FE > nul if errorlevel 1 goto test_fail fec.exe --dump-ast STD\CORE.FE > nul @@ -37,11 +37,11 @@ if errorlevel 1 goto test_fail fec.exe --dump-ast STD\SYS.FE > nul if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\MISSIN.FE > nul +fec.exe --dump-ast TESTS\FAIL\MISSING-SEMI.FE > nul if not errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\UNCLOS.FE > nul +fec.exe --dump-ast TESTS\FAIL\UNCLOSED-COMMENT.FE > nul if not errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\LOGICA.FE > nul +fec.exe --dump-ast TESTS\FAIL\LOGICAL-SYMBOLS.FE > nul if not errorlevel 1 goto test_fail if exist TESTS\M2\HELLO.C del TESTS\M2\HELLO.C @@ -67,30 +67,30 @@ TESTS\M2\SCOPES.EXE if errorlevel 1 goto test_fail rem M2 bits16 regression path remains on compiler A (wcl). -fec.exe --target=bits16 --emit-c TESTS\M2\CAST-W.FE -o TESTS\M2\CAST16.C > nul +fec.exe --target=bits16 --emit-c TESTS\M2\CAST-WHILE.FE -o TESTS\M2\CAST16.C > nul if errorlevel 1 goto test_fail wcl -q -za -bt=dos -fe=TESTS\M2\CAST16.EXE TESTS\M2\CAST16.C if errorlevel 1 goto test_fail TESTS\M2\CAST16.EXE if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CO.FE -o TESTS\M2\BAD-CO.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CONDITION.FE -o TESTS\M2\BAD-CO.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CA.FE -o TESTS\M2\BAD-CA.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CAST.FE -o TESTS\M2\BAD-CA.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-AS.FE -o TESTS\M2\BAD-AS.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ASSIGN.FE -o TESTS\M2\BAD-AS.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UN.FE -o TESTS\M2\BAD-UN.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNKNOWN.FE -o TESTS\M2\BAD-UN.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-AR.FE -o TESTS\M2\BAD-AR.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ARITY.FE -o TESTS\M2\BAD-AR.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-TY.FE -o TESTS\M2\BAD-TY.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-TYPES.FE -o TESTS\M2\BAD-TY.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-RE.FE -o TESTS\M2\BAD-RE.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-RETURN.FE -o TESTS\M2\BAD-RE.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UI.FE -o TESTS\M2\BAD-UI.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNINIT.FE -o TESTS\M2\BAD-UI.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-VO.FE -o TESTS\M2\BAD-VO.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-VOID.FE -o TESTS\M2\BAD-VO.C > nul if not errorlevel 1 goto test_fail if exist TESTS\M3\STRUCT.C del TESTS\M3\STRUCT.C @@ -197,7 +197,7 @@ wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\FORMAT.EXE TESTS\M4\FORMAT.C if errorlevel 1 goto test_fail TESTS\M4\FORMAT.EXE > nul if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\TRY-FPR.FE -o TESTS\M4\TRY-FPR.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\TRY-FPRINT.FE -o TESTS\M4\TRY-FPR.C > nul if errorlevel 1 goto test_fail wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\TRY-FPR.EXE TESTS\M4\TRY-FPR.C if errorlevel 1 goto test_fail @@ -209,17 +209,17 @@ wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\PROP.EXE TESTS\M4\PROPTEST.C if errorlevel 1 goto test_fail TESTS\M4\PROP.EXE > nul if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-ARI.FE -o TESTS\M4\BAD-ARI.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-ARITY.FE -o TESTS\M4\BAD-ARI.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-VERB.FE -o TESTS\M4\BAD-VERB.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-RUN.FE -o TESTS\M4\BAD-RUN.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-RUNTIME.FE -o TESTS\M4\BAD-RUN.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TYP.FE -o TESTS\M4\BAD-TYP.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TYPE.FE -o TESTS\M4\BAD-TYP.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TRY.FE -o TESTS\M4\BAD-TRY.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-WRI.FE -o TESTS\M4\BAD-WRI.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-WRITER.FE -o TESTS\M4\BAD-WRI.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-MANY.FE -o TESTS\M4\BAD-MANY.C > nul if not errorlevel 1 goto test_fail @@ -233,7 +233,13 @@ fec.exe --target=bits32 --emit-c TESTS\M5\OWNED.FE -o TESTS\M5\OWNED.C > nul if errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M5\BAD-MOVE.FE -o TESTS\M5\BAD-MOVE.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DES.FE -o TESTS\M5\BAD-DES.C > nul +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DESTROY.FE -o TESTS\M5\BAD-DES.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DROP.FE -o TESTS\M5\BAD-DROP.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DOUBLE.FE -o TESTS\M5\BAD-DBL.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-CONDITIONAL.FE -o TESTS\M5\BAD-COND.C > nul if not errorlevel 1 goto test_fail if exist TESTS\M5\RUNTIME-G.C del TESTS\M5\RUNTIME-G.C if exist TESTS\M5\RUNTIME.O del TESTS\M5\RUNTIME.O @@ -242,7 +248,7 @@ fec.exe --target=bits32 --emit-c TESTS\M5\RUNTIME.FE -o TESTS\M5\RUNTIME-G.C > n if errorlevel 1 goto test_fail rem Compile generated source and the C89 runtime harness in one WCL386 invocation rem so both objects use the same DOS/4GW startup and runtime library. -wcl386 -q -za -bt=dos -fe=TESTS\M5\RUNTIME.EXE TESTS\M5\RUNTIME-G.C TESTS\M5\RUNTIME.C +wcl386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\M5\RUNTIME.EXE TESTS\M5\RUNTIME-G.C TESTS\M5\RUNTIME.C if errorlevel 1 goto test_fail TESTS\M5\RUNTIME.EXE if errorlevel 1 goto test_fail diff --git a/fec/tests/m5/bad-conditional.fe b/fec/tests/m5/bad-conditional.fe new file mode 100644 index 0000000..90582bc --- /dev/null +++ b/fec/tests/m5/bad-conditional.fe @@ -0,0 +1,8 @@ +unit m5_bad_conditional; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(p: ^i32, flag: bool) -> void { + if flag { take(p); } + p.^ = 3; +} diff --git a/fec/tests/m5/bad-double.fe b/fec/tests/m5/bad-double.fe new file mode 100644 index 0000000..32f4b2b --- /dev/null +++ b/fec/tests/m5/bad-double.fe @@ -0,0 +1,6 @@ +unit m5_bad_double; + +fn bad(p: ^i32) -> void { + mem.destroy(p); + mem.destroy(p); +} diff --git a/fec/tests/m5/bad-drop.fe b/fec/tests/m5/bad-drop.fe new file mode 100644 index 0000000..7587db6 --- /dev/null +++ b/fec/tests/m5/bad-drop.fe @@ -0,0 +1,11 @@ +unit m5_bad_drop; + +struct Box { + value: i32, + fn drop(self: &mut Self) { self.value = 0; } +} + +fn bad() -> void { + var b: Box = Box{ value: 1 }; + b.drop(); +} diff --git a/fec/tests/m5/runtime.c b/fec/tests/m5/runtime.c index 2f3a110..04d5c10 100644 --- a/fec/tests/m5/runtime.c +++ b/fec/tests/m5/runtime.c @@ -1,9 +1,52 @@ +#include +#include + +#undef malloc +#undef free + extern long fe_m5_runtime_run(long mode); +extern void fe_m5_runtime_conditional(unsigned char flag); +extern void fe_m5_runtime_argument_cleanup(void); + +static void *live_ptrs[64]; +static unsigned live_count; +static unsigned alloc_count; +static unsigned free_count; +static unsigned double_free_count; + +void *m5_malloc(size_t size) +{ + void *p = malloc(size); + if (p && live_count < 64) live_ptrs[live_count++] = p; + if (p) ++alloc_count; + return p; +} + +void m5_free(void *p) +{ + unsigned i; + if (!p) return; + for (i = 0; i < live_count; ++i) { + if (live_ptrs[i] == p) { + live_ptrs[i] = live_ptrs[--live_count]; + ++free_count; + free(p); + return; + } + } + ++double_free_count; +} int main(void) { if (fe_m5_runtime_run(0) != 0) return 1; if (fe_m5_runtime_run(1) != 9) return 2; if (fe_m5_runtime_run(2) != 0) return 3; + fe_m5_runtime_conditional(0); + fe_m5_runtime_conditional(1); + fe_m5_runtime_argument_cleanup(); + if (double_free_count != 0) return 4; + if (live_count != 0) return 5; + if (alloc_count != free_count) return 6; return 0; } diff --git a/fec/tests/m5/runtime.fe b/fec/tests/m5/runtime.fe index be03a9d..7df03b6 100644 --- a/fec/tests/m5/runtime.fe +++ b/fec/tests/m5/runtime.fe @@ -1,5 +1,7 @@ unit m5_runtime; +fn take(p: ^i32) -> void { mem.destroy(p); } + pub fn run(mode: i32) -> i32 { var p: ^i32 = try mem.create(i32); defer { mem.destroy(p); } @@ -9,11 +11,17 @@ pub fn run(mode: i32) -> i32 { p.^ = 9; return p.^; } - while true { - break; - } - if mode == 2 { - return 0; - } + while true { break; } + if mode == 2 { return 0; } return p.^ - 7; } + +pub fn conditional(flag: bool) -> void { + var p: ^i32 = try mem.create(i32); + if flag { take(p); } +} + +pub fn argument_cleanup() -> void { + let p: ^i32 = try mem.create(i32); + take(p); +} From c57fa9441d8cda9837ca019b8f18262d3e3884e9 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:57:03 +0900 Subject: [PATCH 028/184] docs: add AGENTS.md and link it from CLAUDE.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HANDOFF.md 제거로 사라진 VM 빌드 함정(large model, *.obj 링크, -wx -wcd=202, 8.3 파일명, D: 빌드 금지, Ctrl+C 복구)을 되살리고 검증 규칙과 현재 마일스톤 상태를 함께 정리한다. 도구 사용법은 tools/README.md로 링크한다. CLAUDE.md는 @AGENTS.md 참조만 둔다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT --- AGENTS.md | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 2 files changed, 67 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..790c97e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,66 @@ +# doslang 작업 규칙 + +DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 문서는 `SPEC.md`이며 +이 파일은 그것을 구현할 때의 작업 규칙만 다룬다. + +## 문서 지도 + +| 파일 | 역할 | +|---|---| +| `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | +| `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | +| `tools/README.md` | QEMU/FreeDOS 자동화 도구 사용법 | +| `tools/tcpagent/README.md` | DOS 내부 TCP 에이전트 프로토콜과 빌드 | + +## 검증 규칙 + +- **실행 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox는 쓰지 않는다. +- 컴파일러 A와 생성 C 모두 VM 안의 Open Watcom으로 컴파일한다. + 컴파일러 A와 bits16은 `WCL`, bits32 생성 C는 `WCL386`. +- **호스트에서 컴파일하지 않는다.** WSL이나 호스트 C 컴파일러 결과는 정식 검증으로 + 인정하지 않는다. 호스트는 편집, diff, Git, 파일 전송에만 쓴다. +- authoritative workspace는 VM의 `C:\FEC`다. +- 마일스톤 완료 기준은 `C:\FEC\BUILD.OK`, `C:\FEC\TEST.OK`, `TEST-DOS.BAT` exit 0 + 세 가지를 모두 확인하는 것이다. 테스트가 증명하지 않는 기능은 완료로 처리하지 + 않는다. +- VGA 데모처럼 멀티모달 수동 검증이 필요한 항목은 완료 게이트에서 제외한다. + +## 빌드 함정 + +VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 실패가 난다. + +- **컴파일러 A는 16비트 large model로 빌드한다.** small model은 메모리 부족으로 + 실패한다. +- **링크는 `*.obj` 와일드카드로 한다.** DOS 명령줄 길이 제한 때문에 오브젝트를 + 나열할 수 없다. 그래서 `fec/build-dos.bat`는 먼저 `C:\FEC\*.obj`를 지워 + stale 32비트 test object가 섞이지 않게 한다. +- **M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다.** 생성 C의 보수적 미사용 helper 때문에 + W202만 끄고 나머지 경고는 오류로 유지한다. +- **fixture는 짧은 이름으로 전송한다.** DOS 8.3 파일명 때문에 긴 이름은 + `BAD-ARI.FE`, `TRY-FPR.FE`처럼 명시적으로 줄여야 한다. +- **`D:`에서 빌드하지 않는다.** QEMU의 vvfat 뷰는 교환용이며, 과거 `D:`에서 빌드하다 + rename 처리 오류로 QEMU가 종료된 적이 있다. 호스트에서 편집한 파일은 `put`으로 + `C:`에 올린 뒤 컴파일한다. +- 긴 DOS 배치가 멈추면 QEMU를 재시작하기 전에 Ctrl+C 주입을 먼저 시도한다. + `Terminate batch file ... (Yes/No/All)?`가 뜨면 `y`, `ret`을 보내고 `ping` 복구를 + 확인한다. + +## 작업 흐름 + +- 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신하고 `SPEC.AUDIT.md`에 사유를 누적한다. + 구현이 명세와 다르면 둘 중 하나가 틀린 것이므로 그 자리에서 결론을 낸다. +- 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. +- primary 브랜치는 `master`다. +- `.qemu/*.png`, `.qemu/*.ppm`은 진단용이며 커밋하지 않는다. + +## 현재 상태 + +- M1~M4 완료 및 QEMU/Open Watcom 검증됨. +- M5(`^T`, drop, defer, 이동 검사) 진행 중. 남은 것: 모든 경로에서 정확히 1회 + cleanup, defer와 drop의 선언 역순 병합, `try` 전파 경로 cleanup, `MaybeMoved` + 런타임 live flag, struct drop과 필드 역순 drop, 분기/루프 상태 합류, + 누수·이중해제 카운터 harness. +- `own.c/h`가 아직 없고 소유권 로직이 `check.c`/`emit_c.c`에 들어가 있다. R1~R8 + 전체를 다루는 **M6 착수 시점에 분리한다** (`SPEC.md` §11.3). +- v0.1.6에서 R8(파생 반환), R6(마지막 사용까지 대여), R10(전역 대여 금지)이 + 바뀌었다. 셋 다 own.c의 상태 기계를 건드리므로 분리 이후에 함께 구현한다. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md From 2e7502bfa636f32b36e58bd87332c21fd0c44493 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 16:58:42 +0900 Subject: [PATCH 029/184] chore: ignore stray Windows nul artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git Bash에서 `> nul`을 실행하면 NUL 장치가 아니라 실제 파일이 생긴다. 커밋되면 Windows 체크아웃이 깨지므로 무시하고 기존 흔적을 지운다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 089da50..ac1e67a 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ .qemu/*.ppm .qemu/*.tmp +# Windows reserved-device artifact: `> nul` under Git Bash creates a real file. +# Committing it breaks checkout on Windows. +nul + # Local logs and editor/OS metadata *.log *.tmp From eff5cbfb120fcb0227db35f1a4ce0d7f7a5c5f59 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 17:01:30 +0900 Subject: [PATCH 030/184] docs: make the CLI the source of truth for ferro-vm commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 명령 목록이 tools/README.md와 argparse 정의 두 곳에 손으로 동기화되고 있었다. 드리프트가 불가피하므로 목록을 --help로 단일화한다. cli.py에 서브커맨드별 help/description, 인자 metavar, 예시 epilog를 채웠다. put의 DOS 8.3 이름 제약처럼 명령에 직접 붙는 함정은 해당 도움말에 넣었다. tools/README.md는 호스트 요구사항, 셋업, 자동화 구조로 줄이고 목록은 --help로 넘긴다. AGENTS.md에는 CLI가 규범이라는 포인터를 남긴다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT --- AGENTS.md | 10 ++++- src/ferrolang_vm/cli.py | 83 +++++++++++++++++++++++++++++++++-------- tools/README.md | 47 +++++++++++------------ 3 files changed, 97 insertions(+), 43 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 790c97e..d48bfeb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,9 +9,17 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 |---|---| | `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | | `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | -| `tools/README.md` | QEMU/FreeDOS 자동화 도구 사용법 | +| `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | | `tools/tcpagent/README.md` | DOS 내부 TCP 에이전트 프로토콜과 빌드 | +VM 자동화 **명령 목록과 플래그는 문서가 아니라 CLI가 규범**이다. 문서에 복제하면 +반드시 드리프트하므로 아래로 확인한다. + +```powershell +uv run ferro-vm --help +uv run ferro-vm --help +``` + ## 검증 규칙 - **실행 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox는 쓰지 않는다. diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py index f268a64..411b144 100644 --- a/src/ferrolang_vm/cli.py +++ b/src/ferrolang_vm/cli.py @@ -64,23 +64,74 @@ def follow_logs() -> int: ]).returncode +EPILOG = r"""examples: + uv run ferro-vm start boot the VM and start the daemon + uv run ferro-vm wait-ready block until TCPAGENT answers PING + uv run ferro-vm exec 'dir C:\FEC' run a DOS command, print exit code and output + uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' + uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK + uv run ferro-vm logs follow the structured daemon log + +The authoritative workspace is C:\FEC inside the VM. Never build on D: (the vvfat +view is for exchange only). See AGENTS.md for verification rules and build traps. +""" + +SIMPLE_COMMANDS = { + "start": "Start the daemon and boot QEMU. Safe to run when already up.", + "stop": "Quit QEMU cleanly and stop the daemon.", + "status": "Print daemon, QEMU, and TCPAGENT connection state as JSON.", + "ping": "Send PING to TCPAGENT. Expects 'OK 504F4E47' (PONG).", + "screenshot": "Capture the VGA console to a PPM/PNG under .qemu/.", + "ocr": "Capture the console and print recognized text (RapidOCR).", + "logs": "Follow the append-only daemon log. Uses lnav when available.", +} + + def main() -> int: - parser = argparse.ArgumentParser(description="Windows-only QEMU/FreeDOS automation") - commands = parser.add_subparsers(dest="op", required=True) - for name in ("start", "stop", "status", "ping", "screenshot", "ocr", "logs"): - commands.add_parser(name) - wait = commands.add_parser("wait-ready") - wait.add_argument("--timeout", type=int, default=45) - reset = commands.add_parser("reset") - reset.add_argument("--timeout", type=int, default=45) - execute = commands.add_parser("exec") - execute.add_argument("command") - put = commands.add_parser("put") - put.add_argument("source", type=Path) - put.add_argument("destination") - get = commands.add_parser("get") - get.add_argument("source") - get.add_argument("destination", type=Path) + parser = argparse.ArgumentParser( + prog="ferro-vm", + description="Windows-only QEMU/FreeDOS automation for the Ferro compiler.", + epilog=EPILOG, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + commands = parser.add_subparsers(dest="op", required=True, metavar="COMMAND") + for name, blurb in SIMPLE_COMMANDS.items(): + commands.add_parser(name, help=blurb, description=blurb) + + wait_help = "Block until TCPAGENT is connected and answers PING." + wait = commands.add_parser("wait-ready", help=wait_help, description=wait_help) + wait.add_argument("--timeout", type=int, default=45, metavar="SECONDS", + help="give up after this many seconds (default: %(default)s)") + + reset_help = "Quit QEMU cleanly, reboot it, and wait for TCPAGENT." + reset = commands.add_parser("reset", help=reset_help, description=reset_help) + reset.add_argument("--timeout", type=int, default=45, metavar="SECONDS", + help="give up after this many seconds (default: %(default)s)") + + exec_help = "Run a DOS command inside the VM and print its exit code and output." + execute = commands.add_parser( + "exec", help=exec_help, + description=exec_help + " Quote the command so the host shell does not eat" + r" backslashes: exec 'wcl386 -q HELLO.C'.") + execute.add_argument("command", metavar="DOS_COMMAND", + help=r"command line to hand to COMMAND.COM, e.g. 'dir C:\FEC'") + + put_help = "Copy a host file into the VM." + put = commands.add_parser("put", help=put_help, description=put_help) + put.add_argument("source", type=Path, metavar="HOST_PATH", + help="file on this machine") + put.add_argument("destination", metavar="DOS_PATH", + help=r"target inside the VM, e.g. 'C:\FEC\SRC\CHECK.C'." + " DOS uses 8.3 names, so long fixtures must be shortened" + " explicitly (BAD-ARI.FE, TRY-FPR.FE)") + + get_help = "Copy a file out of the VM onto the host." + get = commands.add_parser("get", help=get_help, description=get_help) + get.add_argument("source", metavar="DOS_PATH", + help=r"file inside the VM, e.g. 'C:\FEC\TEST.OK'") + get.add_argument("destination", type=Path, metavar="HOST_PATH", + help="target on this machine") + args = parser.parse_args() try: diff --git a/tools/README.md b/tools/README.md index dd535d7..3d909a5 100644 --- a/tools/README.md +++ b/tools/README.md @@ -6,46 +6,41 @@ Automation currently supports **Windows 10/11 only**. It requires `uv`, QEMU with WHPX support, and `ffmpeg.exe` on `PATH`. The Python implementation uses portable APIs where possible, but other hosts are not supported yet. -## QEMU and FreeDOS automation - -Start the Python daemon and QEMU: +## Getting started ```powershell uv run ferro-vm start uv run ferro-vm status ``` -`TCPAGENT.EXE` connects only to `127.0.0.1:5558`. Local commands use the -Windows named pipe `\\.\pipe\ferrolang-vm`; there is no controller or observer -TCP port. Monitor the append-only structured log in another terminal: +The command list lives in the CLI itself, not in this file: ```powershell -uv run ferro-vm logs +uv run ferro-vm --help +uv run ferro-vm --help ``` -Commands: +Working rules, verification gates, and DOS build traps are in `AGENTS.md`. -```powershell -uv run ferro-vm reset # clean QEMU quit and restart -uv run ferro-vm wait-ready --timeout 45 -uv run ferro-vm ping -uv run ferro-vm exec 'dir C:\FEC' -uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' -uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK -uv run ferro-vm screenshot -uv run ferro-vm ocr -uv run ferro-vm stop -``` +## How it fits together -`reset` cleanly quits and restarts QEMU, waits for FreeDOS to boot, submits -the default boot-menu Enter, and requires TCPAGENT `PING`/`PONG`. QEMU -`system_reset` is intentionally unsupported because repeated soft resets leave -the FreeDOS NE2000 packet driver stuck during initialization. `logs` starts -`lnav` when installed and otherwise falls back to PowerShell `Get-Content --Wait`. The daemon logs command metadata, DOS output, exit status, transfers, -and agent lifecycle events as UTF-8 lines. It deliberately never logs raw binary +`TCPAGENT.EXE` runs inside FreeDOS and dials out to `127.0.0.1:5558`; its wire +protocol is documented in `tcpagent/README.md`. The `ferro-vm` daemon owns that +connection and the QEMU monitor. Local commands reach the daemon over the +Windows named pipe `\\.\pipe\ferrolang-vm` — there is no controller or observer +TCP port. + +The daemon writes an append-only structured log (`uv run ferro-vm logs`, which +uses `lnav` when installed and otherwise falls back to PowerShell `Get-Content +-Wait`). It records command metadata, DOS output, exit status, transfers, and +agent lifecycle events as UTF-8 lines, and deliberately never logs raw binary payloads or protocol hex. +`reset` quits QEMU cleanly, restarts it, waits for FreeDOS to boot, submits the +default boot-menu Enter, and requires a TCPAGENT `PING`/`PONG` before returning. +QEMU `system_reset` is intentionally unsupported: repeated soft resets leave the +FreeDOS NE2000 packet driver stuck during initialization. + ## Standalone OCR `tools/qemu_ocr.py` remains available for OCRing an existing image: From 8c3dee222e9e2d256bd2b4275f3761207a86535f Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 17:13:05 +0900 Subject: [PATCH 031/184] feat: color the agent console and log every command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOS 콘솔에서 직접 보기 위한 네 가지 수정. 1. 컬러. Open Watcom의 conio.h에는 textattr()이 없고(cprintf/cputs/getch만 제공) 이 FreeDOS 콘솔은 ANSI 이스케이프도 해석하지 않는다. 그래서 cprintf가 줄을 배치하게 두고 -- 스크롤을 알아서 처리한다 -- 방금 쓴 셀의 VGA 속성 바이트만 다시 칠한다. 커서가 그 줄 다음 행의 0열에 있다는 점을 이용해 스크롤을 직접 추적하지 않고 대상 셀을 찾는다. 줄바꿈된 긴 줄도 처리한다. 2. GET, HASH, LIST, READ, WRITE, QUIT, 미지원 명령을 로깅한다. 이전에는 EXEC과 PUT만 보였다. PING은 wait-ready가 초당 두 번 폴링하므로 제외한다. LIST는 잘림 여부를, PUT은 short write를 구분해 남긴다. 3. put_path가 채워지기만 하고 쓰이지 않아 PUT 완료 줄에 경로가 없었다. 완료 경로가 둘(길이 0, 본문 수신 완료)이라 put_finished()로 합쳤다. 4. 틱->초 변환이 1.1% 빨랐다. BIOS 틱은 18.2065Hz이므로 delta/18이 아니라 delta*549/100 (하루치 틱에도 32비트를 넘지 않는다) 을 쓴다. REBUILD.BAT을 추가한다. BUILD.BAT은 현재 디렉터리에서 wmake만 실행하는데 호스트가 exec으로 부를 때의 시작 디렉터리가 거기가 아니다. QEMU FreeDOS에서 Open Watcom C++16으로 빌드하고(no warnings, -we 활성) 콘솔 스크린샷으로 색상, 줄바꿈 색칠, 명령별 로그를 확인했다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT --- tools/tcpagent/README.md | 44 ++++++++++++-- tools/tcpagent/REBUILD.BAT | 22 +++++++ tools/tcpagent/tcpagent.cpp | 113 +++++++++++++++++++++++++----------- 3 files changed, 140 insertions(+), 39 deletions(-) create mode 100644 tools/tcpagent/REBUILD.BAT diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md index 2261744..0b2487f 100644 --- a/tools/tcpagent/README.md +++ b/tools/tcpagent/README.md @@ -40,7 +40,43 @@ uv run ferro-vm put host-file 'C:\DOS\FILE' uv run ferro-vm get 'C:\DOS\FILE' host-file ``` -The foreground agent shows timestamped connection, transfer, and command -start/finish lines. It also keeps the same metadata in `C:\TCPAGENT.LOG`, -rotating files larger than 256 KiB to `C:\TCPAGENT.OLD`. Payloads and command -output are never written to that metadata log. +## Agent-side logging + +The foreground agent prints one timestamped line per event on the VGA console +and keeps the same text in `C:\TCPAGENT.LOG`, rotating files larger than 256 KiB +to `C:\TCPAGENT.OLD`. Payloads and command output are never written to that +metadata log. + +Every command is logged with a request line and a result line carrying byte +counts and elapsed time — `EXEC`, `PUT`, `GET`, `HASH`, `LIST`, `READ`, and +`WRITE`. `PING` is deliberately excluded because `wait-ready` polls it twice a +second. Connection events (`connecting`, `connected`, `connect failed; retry N`, +`link lost`) are logged too; those are invisible to the host by definition, +since they happen when the socket is down. + +Lines are colored by writing VGA attribute bytes after `cprintf` lays out the +line: gray timestamps, cyan requests, yellow `EXEC` command text, green success, +red failure. Open Watcom's DOS `conio.h` has no `textattr()`, and ANSI escapes +are not interpreted on this FreeDOS console, so neither of the usual routes +works. Elapsed times come from the BIOS tick counter at 18.2065 Hz (~55 ms +resolution). + +Note that mTCP is not driven while `system()` runs a child, so a DOS command +lasting tens of seconds can drop the TCP connection. The agent logs `link lost` +and reconnects on its own, but the host loses that command's result. + +## Rebuilding inside the VM + +`REBUILD.BAT` compiles and installs the agent in a single `exec`. `BUILD.BAT` +only runs `wmake` in the current directory, which is not where a host-driven +`ferro-vm exec` starts. + +```powershell +uv run ferro-vm put tools/tcpagent/tcpagent.cpp 'C:\MTSRC\MTCP\APPS\TCPAGENT\TCPAGENT.CPP' +uv run ferro-vm put tools/tcpagent/REBUILD.BAT 'C:\REBUILD.BAT' +uv run ferro-vm exec 'C:\REBUILD.BAT' +uv run ferro-vm reset +``` + +The reset is required: the running agent holds the old image in memory, and +`C:\FDAUTO.BAT` starts it at boot. diff --git a/tools/tcpagent/REBUILD.BAT b/tools/tcpagent/REBUILD.BAT new file mode 100644 index 0000000..989d172 --- /dev/null +++ b/tools/tcpagent/REBUILD.BAT @@ -0,0 +1,22 @@ +@echo off +rem Rebuild TCPAGENT.EXE from C:\MTSRC and install it, in one EXEC. +rem BUILD.BAT only runs wmake in the current directory, which is not where a +rem host-driven `ferro-vm exec` starts. Copy this to C:\ and run it by path. +rem The running agent keeps the old image in memory, so reset the VM afterwards: +rem uv run ferro-vm reset +C: +cd C:\MTSRC\MTCP\APPS\TCPAGENT +set WATCOM=C:\DEVEL\WATCOMC +set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN +set INCLUDE=C:\DEVEL\WATCOMC\H +set EDPATH= +if exist TCPAGENT.OBJ del TCPAGENT.OBJ +if exist TCPAGENT.EXE del TCPAGENT.EXE +wmake +if not exist TCPAGENT.EXE goto fail +copy /Y TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE +echo BUILD-OK +goto end +:fail +echo BUILD-FAILED +:end diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp index 2aff406..574316d 100644 --- a/tools/tcpagent/tcpagent.cpp +++ b/tools/tcpagent/tcpagent.cpp @@ -38,17 +38,42 @@ static unsigned long ticks(void) { _bios_timeofday(_TIME_GETCLOCK,&value); return (unsigned long)value; } +/* The BIOS tick is 18.2065 Hz, so one tick is 5.49254 hundredths of a second. + 549/100 keeps the error under 0.05% and cannot overflow 32 bits for a delta + up to a full day (1573040 ticks * 549 fits). Plain delta/18 ran 1.1% fast. */ static void elapsed_text(unsigned long started,char *out) { unsigned long now=ticks(),delta=now>=started?now-started:now+(1573040UL-started); - sprintf(out,"%lu.%02lus",delta/18UL,(delta%18UL)*100UL/18UL); + unsigned long hundredths=delta*549UL/100UL; + sprintf(out,"%lu.%02lus",hundredths/100UL,hundredths%100UL); +} + +/* Open Watcom's DOS conio has no textattr()/textcolor() -- it offers only + cprintf/cputs/getch and friends -- and ANSI escapes are not interpreted on + the installed FreeDOS console. So let cprintf lay the line out (it scrolls + correctly) and then repaint the attribute bytes of the cells it just wrote. + The cursor sits at column 0 of the row after the line, which is what lets us + find those cells without tracking scrolling ourselves. */ +#define TIMESTAMP_WIDTH 9 +static void colorize(unsigned total,int attr) { + unsigned char __far *vram; unsigned cols,used,row,col,start,k; + if(*(unsigned char __far *)MK_FP(0x0040,0x0049)==7) return; /* MDA: no color */ + cols=*(unsigned __far *)MK_FP(0x0040,0x004A); + if(cols<40||cols>132) cols=80; + used=(total+cols-1)/cols; if(!used) used=1; + row=*(unsigned char __far *)MK_FP(0x0040,0x0051); + if(row READ %s @%ld",path,pos); + f=fopen(path,"rb"); if(!f){log_line(0x0C,"< READ ERR cannot open");error_text("Cannot open file");return;} + if(fseek(f,pos,SEEK_SET)){fclose(f);log_line(0x0C,"< READ ERR cannot seek");error_text("Cannot seek file");return;} count=fread(data,1,CHUNK_SIZE,f); eof=count WRITE %s %c %dB",path,mode[0]=='A'?'A':'T',count); + f=fopen(path,mode[0]=='A'?"ab":"wb"); if(!f){log_line(0x0C,"< WRITE ERR cannot open");error_text("Cannot write file");return;} + if(count&&fwrite(data,1,count,f)!=(size_t)count){fclose(f);log_line(0x0C,"< WRITE ERR short write");error_text("Short write");return;} + fclose(f); log_line(0x0A,"< WRITE OK"); ok_data((const unsigned char *)"",0); } static void command_put(char *args) { char path[260],*length_text=strchr(args,' '); - if(!length_text){error_text("PUT requires path and length");return;} + if(!length_text){log_line(0x0C,"< PUT ERR missing length");error_text("PUT requires path and length");return;} *length_text++='\0'; - if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;} + if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< PUT ERR bad path encoding");error_text("Invalid path encoding");return;} put_remaining=strtoul(length_text,0,10); strcpy(put_path,path); put_started=ticks(); - log_line(0x0B,"> PUT %s %luB",path,put_remaining); + log_line(0x0B,"> PUT %s %luB",put_path,put_remaining); put_file=fopen(path,"wb"); - if(!put_file){put_remaining=0;log_line(0x0C,"< PUT ERR cannot open");error_text("Cannot write file");return;} - if(!put_remaining){char elapsed[24];fclose(put_file);put_file=0;elapsed_text(put_started,elapsed);log_line(0x0A,"< PUT OK %s",elapsed);ok_data((const unsigned char *)"",0);} + if(!put_file){put_remaining=0;log_line(0x0C,"< PUT %s ERR cannot open",put_path);error_text("Cannot write file");return;} + if(!put_remaining){fclose(put_file);put_file=0;put_finished();ok_data((const unsigned char *)"",0);} } static void command_get(char *args) { - char path[260]; FILE *f; long length; size_t count; - if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;} - f=fopen(path,"rb"); if(!f){error_text("Cannot open file");return;} + char path[260],elapsed[24]; FILE *f; long length; size_t count; unsigned long started=ticks(); + if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< GET ERR bad path encoding");error_text("Invalid path encoding");return;} + log_line(0x0B,"> GET %s",path); + f=fopen(path,"rb"); if(!f){log_line(0x0C,"< GET %s ERR cannot open",path);error_text("Cannot open file");return;} fseek(f,0,SEEK_END); length=ftell(f); fseek(f,0,SEEK_SET); sprintf(linebuf,"DATA %ld\r\n",length); write_text(linebuf); while((count=fread(data,1,CHUNK_SIZE,f))>0) if(send_all(data,count)<0)break; - fclose(f); + fclose(f); elapsed_text(started,elapsed); + log_line(0x0A,"< GET OK %ldB %s",length,elapsed); } static void command_hash(char *args) { - char path[260]; FILE *f; size_t count; unsigned i; unsigned long length=0,hash=2166136261UL; - if(!decode_path(args,path,sizeof(path))){error_text("Invalid path encoding");return;} - f=fopen(path,"rb"); if(!f){error_text("Cannot open file");return;} + char path[260],elapsed[24]; FILE *f; size_t count; unsigned i; + unsigned long length=0,hash=2166136261UL,started=ticks(); + if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< HASH ERR bad path encoding");error_text("Invalid path encoding");return;} + log_line(0x0B,"> HASH %s",path); + f=fopen(path,"rb"); if(!f){log_line(0x0C,"< HASH %s ERR cannot open",path);error_text("Cannot open file");return;} while((count=fread(data,1,CHUNK_SIZE,f))>0){length+=(unsigned long)count;for(i=0;i LIST %s",path); strcpy(pattern,path); if(pattern[0]&&pattern[strlen(pattern)-1]!='\\') strcat(pattern,"\\"); strcat(pattern,"*.*"); rc=_dos_findfirst(pattern,_A_NORMAL|_A_RDONLY|_A_HIDDEN|_A_SYSTEM|_A_SUBDIR|_A_ARCH,&found); while(rc==0) { char entry[100]; int len; - if(strcmp(found.name,".")&&strcmp(found.name,"..")) { sprintf(entry,"%s\t%lu\t%s\n",found.name,found.size,(found.attrib&_A_SUBDIR)?"DIR":"FILE"); len=strlen(entry); if(used+(unsigned)len>=sizeof(output)) break; memcpy(output+used,entry,len); used+=(unsigned)len; } + if(strcmp(found.name,".")&&strcmp(found.name,"..")) { sprintf(entry,"%s\t%lu\t%s\n",found.name,found.size,(found.attrib&_A_SUBDIR)?"DIR":"FILE"); len=strlen(entry); if(used+(unsigned)len>=sizeof(output)) {truncated=1;break;} memcpy(output+used,entry,len); used+=(unsigned)len; ++entries; } rc=_dos_findnext(&found); } + log_line(truncated?0x0E:0x0A,"< LIST %u entries%s",entries,truncated?" (truncated)":""); ok_data((unsigned char *)output,used); } static void command_exec(char *args) { @@ -202,6 +244,7 @@ static void command_exec(char *args) { } static void process_line(char *line) { char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd); + /* PING is deliberately not logged: wait-ready polls it twice a second. */ if(!strcmp(cmd,"PING"))ok_data((const unsigned char *)"PONG",4); else if(!strcmp(cmd,"READ"))command_read(args); else if(!strcmp(cmd,"WRITE"))command_write(args); @@ -210,8 +253,8 @@ static void process_line(char *line) { else if(!strcmp(cmd,"HASH"))command_hash(args); else if(!strcmp(cmd,"LIST"))command_list(args); else if(!strcmp(cmd,"EXEC"))command_exec(args); - else if(!strcmp(cmd,"QUIT")){ok_data((const unsigned char *)"BYE",3);stop_requested=1;} - else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); error_text(message); } + else if(!strcmp(cmd,"QUIT")){log_line(0x07,"* QUIT received");ok_data((const unsigned char *)"BYE",3);stop_requested=1;} + else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); log_line(0x0C,"< ERR %s",message); error_text(message); } } static int connect_host(void) { IpAddr_t host={10,0,2,2}; int8_t rc; @@ -241,10 +284,10 @@ int main(void) { if(put_remaining) { unsigned available=(unsigned)(rc-i); unsigned take=put_remaining Date: Sun, 16 Aug 2026 17:29:45 +0900 Subject: [PATCH 032/184] docs: resolve ownership and standard library spec audit --- SPEC.AUDIT.md | 111 +++++++++++++++++++ SPEC.md | 296 ++++++++++++++++++++++++++------------------------ 2 files changed, 263 insertions(+), 144 deletions(-) diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index 95a2cdf..09f4101 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -154,3 +154,114 @@ M6(borrow checker) 착수 전에 확정해야 하는 소유권·참조 규칙 - `HANDOFF.md`를 제거했다. 여기에만 있던 빌드 함정(컴파일러 A는 16비트 large model, 링크는 `*.obj`, M4 Watcom 테스트는 `-wx -wcd=202`, 8.3 파일명 제약, `D:`에서 빌드 금지)은 별도 문서로 옮겨야 한다. + +## 2026-08-16 — v0.1.7 + +외부 전면 audit에서 발견된 안전성 모순과 M6~M10 구현 전 미결정 사항을 통합했다. +이 절의 결정은 v0.1.6의 `str` nominal 타입, 함수 포인터 Writer, R8 단일 파라미터 +규칙 및 own.c 스코프 끝 해제 결정을 명시적으로 대체한다. + +### 슬라이스·문자열·소유 버퍼 + +- 문제: R4는 일반 `^T`의 대상에 `[]T`를 금지하면서 `^[]T`, `List.items`, + `mem.alloc_slice`를 요구했다. 또한 하나뿐인 `[]T`가 읽기/쓰기를 모두 나타내어 + R6의 공유·배타 대여를 표현할 수 없었다. +- 결정: `[]T`는 공유·읽기 전용, `[]mut T`는 배타·쓰기 가능 slice다. var place만 + mutable slice를 만들 수 있다. 호출 인자 위치의 `[]mut T → []T`, `&mut T → &T`는 + 원래 Exclusive 상태를 유지하는 암묵 재대여로 한정한다. +- 결정: `^[]T`는 일반 포인터 합성이 아닌 `(ptr,len)` 독립 소유 타입이다. + `^[]T`/`?^[]T`만 R4의 대상 제한에서 예외이고 `*[]T`/`*[]mut T`는 금지한다. +- 재검토: v0.1.6은 문자열 리터럴의 불변성을 위해 `str`을 nominal 타입으로 만들었지만, + `[]T` 자체가 불변이 되면서 근거가 사라졌다. `str`을 미리 선언된 `[]u8` 완전 동일 + alias로 내렸다. UTF-8 검증은 없으며 별도 cast·C 표현·쓰기 금지 규칙이 필요 없다. +- 결과: `str`도 R4를 그대로 적용받아 field/element에 저장할 수 없다. 문자열을 + 소유하려면 `String{ bytes: ^[]u8 }`를 쓰며 `as_str()`은 파생 shared slice를 반환한다. + +### 안전한 Writer/Reader와 포매팅 분리 + +- 문제: 안전한 `File.writer() -> Writer`가 대여 대상을 `*void`에 숨겨 반환하여 + `make() -> Writer`만으로 safe-code dangling을 만들 수 있었다. 문서의 "사용자 책임"은 + §1의 memory-safety 보장과 충돌하며 Reader도 동일하게 불건전했다. +- 원칙: 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 + 반환할 수 없다. R9 내부에서 unsafe 변환을 한 번 감쌌다는 사실은 safe API를 + 건전하게 만들지 않는다. +- 결정: v0.1.2 Writer/Reader는 함수 포인터 struct 대신 정수 payload만 가진 Copy handle + enum이다. `Writer{Stdout,Stderr,File(u16),Null}`, `Reader{Stdin,File(u16)}`와 + `io.write(Writer, []u8)`, `io.read(Reader, []mut u8)`를 쓴다. fd 재사용은 논리적 I/O + 오류일 수 있으나 memory dangling은 아니다. buffer Writer는 두지 않는다. +- 결정: fmt는 sink를 모른다. `fmt.fmt_int_i32(tmp: []mut u8, v) -> str`처럼 임시 + buffer에 쓰고 R8(a) 파생 slice를 반환하는 순수 함수 한 벌만 둔다. `@print`/`@fprint`는 + 결과를 `io.write`, `@sprint`는 `mem.copy`로 이어 붙인다. v0.2의 `dyn Writer` 전환은 + 안전성 수정이 아닌 기능 확장이다. + +### 소유권·R4·R8 + +- `str`/`[]T`/조건부·에러 union/배열의 재귀 Copy 규칙을 완성하고 `[]mut T`와 + `^[]T`는 non-Copy로 정했다. +- field/index/optional projection에서 non-Copy 값을 부분 이동하는 것을 금지했다. + own.c는 변수 단위 상태를 유지하며 `mem.replace(&mut place, replacement)`만 추출을 + 허용한다. `.?`/field/index는 값을 즉시 꺼내는 연산이 아니라 place projection이다. +- mutable borrow·slice와 `&mut Self` 호출은 var place에서만 허용한다. consuming + `self: Self`는 메서드 내부에서 invalid sentinel을 남길 수 있는 local owner다. +- R8 메서드는 파생 원본을 self로 고정한다. 추가 참조 인자는 받을 수 있지만 반환이 + 그 인자에서 파생될 수 없다. 자유 함수만 참조성 파라미터 정확히 하나를 요구한다. + `?&T`, `?&mut T`, `?[]T`, `?[]mut T` 반환을 포함한다. +- own.c의 오래된 "스코프 끝 해제"와 "R8 결과 바인딩 거부"를 삭제했다. 역방향 + liveness pass로 마지막 사용을 계산하고 defer 사용은 스코프 끝까지 연장한다. + +### drop, File, heap 초기화 + +- `File.close(=drop)` 모순을 제거했다. `close(self: Self) -> !void`는 소비하는 일반 + 메서드이며 내부 handle을 invalid로 만든 후 오류를 반환한다. 자동 drop은 열린 handle만 + 조용히 닫는다. `drop` 직접 호출 금지는 유지한다. +- `mem.create(T) -> !^T`는 초기화되지 않은 안전 힙을 반환하므로 삭제했다. + `mem.create(value: T) -> !^T`로 바꾸고 T는 값에서 추론한다. + +### error와 결정적 build + +- `try`는 operand와 현재 함수의 nominal error 타입이 같을 때만 허용한다. 다른 타입은 + catch에서 명시 매핑한다. `catch`는 error-return 함수 밖에서도 허용하며 void 결과의 + handler block은 정상 fallthrough할 수 있다. +- 정렬 기반 error code 표는 결정성과 fixpoint를 위해 유지한다. build-directory 이력에 + 의존하는 append-only 표는 기각했다. +- 드라이버는 단일 `fe_errors.h`에 정렬된 `#define`을 생성한다. 이름 집합 변경 시 유닛 + C를 재방출하지 않고 header 의존 object만 다시 컴파일한다. switch 상수 요건도 유지한다. + +### 제네릭·증분 build + +- 제네릭 이름 해석은 사용 유닛이 아니라 정의 유닛 scope에서 한다. `.fei`는 본문 token과 + generic 전용 private signature를 함께 기록한다. +- driver가 전체 인스턴스 요청을 합쳐 단일 `fe_generics.c`에 중복 없이 방출한다. + 사용 유닛별 external 중복 심볼과 static 코드 복제를 모두 피한다. +- comptime type 비교와 최소 introspection `@is_int`, `@is_ptr`를 추가했다. +- `.fei` cache key에 source hash뿐 아니라 dependency `.fei` hash를 포함한다. + +### interrupt/shared와 panic + +- shared C 방출을 `volatile`로 정하고 critical 진입·이탈에 compiler barrier를 둔다. + bits16의 한 명령 크기 8/16비트 atomic load/store는 interrupt 경계에서 원자적이므로 + 자동 critical 없이 volatile 한 명령만 방출한다. far pointer와 RMW는 explicit critical이다. +- `interrupt_safe`에서 critical, port/volatile builtin, asm과 필요한 unsafe를 허용한다. + 금지 목록은 heap, DOS/DPMI, blocking I/O, FPU, non-interrupt-safe 호출로 한정했다. +- panic은 일반 defer unwind를 하지 않지만 interrupt vector 복원용 고정 크기 + `sys.on_exit` callback을 실행한다. bits32 interrupt/shared/critical은 v0.2로 명시했다. + +### 문법·표기 정리 + +- generic struct/enum parameter, declaration-level comptime if, for 전용 range, + bool/char pattern, `[]mut T`를 EBNF에 추가했다. +- 정의되지 않은 단항 `^`를 삭제했다. field/index/slice/method의 `&`/`&mut`/`^` + projection과 raw/optional의 비자동 역참조를 명문화했다. +- method가 function-pointer field보다 우선하며 field 호출은 `(x.f)(...)`로 고정했다. +- `@seg_ptr(T, seg, off)`로 타입 인자를 명시하고 type-valued const alias를 허용했다. +- 단항 직후 cast는 `(-x) as T` 또는 `-(x as T)`처럼 괄호를 강제한다. + +### 구현 및 milestone 영향 + +- M3: shared/mutable slice와 str alias를 재검증한다. +- M4: 함수 포인터 Writer를 handle enum + 순수 fmt 함수로 교체한다. +- M5: `^[]T`, consuming close, projection 부분 이동, 초기화된 create를 반영한다. +- M6: 역방향 liveness와 self-source R8을 구현한다. +- M7: try nominal error 일치와 일반 catch를 구현한다. +- M8/M9: `fe_errors.h`, dependency hash, `fe_generics.c`를 구현한다. +- M10: volatile/barrier, interrupt-safe 허용 목록, on_exit 복원을 검증한다. diff --git a/SPEC.md b/SPEC.md index b153468..80a869a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.6 +# Ferro 언어 명세 v0.1.7 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -64,8 +64,8 @@ and or not orelse - `bool` (1바이트, 정수와 상호 변환 없음) - `char` (`u8`과 크기 같지만 별개 타입). `char`와 `u8` 사이의 저장·대입·비교에는 반드시 명시적인 `as` 변환이 필요하며, 리터럴에도 문맥 기반 암묵 변환을 적용하지 않는다. -- `void` (반환 타입으로만) -- `type` (comptime 파라미터에서만, §9) +- `void` (반환 타입으로만. 단, 역참조 불가능한 `*void`/`far *void`의 대상 타입은 허용, R9) +- `type` (comptime 파라미터와 type alias의 `const` 초기값에서만, §4.7·§9) **정수 규칙:** - 서로 다른 정수 타입 간 암묵 변환 없음. `as`로 명시. @@ -80,21 +80,24 @@ and or not orelse | 문법 | 의미 | 표현 | |---|---|---| | `[N]T` | 배열, 값 타입, N은 컴파일타임 상수 | `N * sizeof(T)` | -| `[]T` | 슬라이스 (참조성, §5 R4 적용) | `(ptr, len)` | -| `str` | 불변 바이트 슬라이스. `[]u8`과 **별개 타입** | `(ptr, len)` | -| `^T` | 소유 포인터 (힙, 단일 소유자) | 포인터 | +| `[]T` | 공유·읽기 전용 슬라이스 (참조성, R4 적용) | `(const ptr, len)` | +| `[]mut T` | 배타·쓰기 가능 슬라이스 (참조성, R4 적용) | `(ptr, len)` | +| `str` | 미리 선언된 `[]u8`의 type alias | `[]u8`과 동일 | +| `^[]T` | 소유 버퍼. 일반 `^T`와 구별되는 독립 소유 타입 | `(ptr, len)` | +| `^T` | 일반 소유 포인터 (힙, 단일 소유자) | 포인터 | | `&T` | 공유 참조 | 포인터 | | `&mut T` | 배타 참조 | 포인터 | | `*T` | raw 포인터 (`unsafe`에서만 역참조) | 포인터 | -| `far ^T`, `far *T`, `far &T` | far 포인터 (`bits16` 전용) | 4바이트 | +| `far ^T`, `far *T`, `far &T`, `far &mut T` | far 포인터 (`bits16` 전용) | 4바이트 | | `?T` | 옵셔널 | 널 표현 가능 타입은 크기 동일, 아니면 `(bool, T)` | | `E!T` / `!T` | 에러 유니온 (`!T`는 기본 에러 집합) | `(u16 err, T val)` | | `fn(A, B) -> R` | 함수 포인터 | 포인터 | - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. -- 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. -- 소유 슬라이스가 필요하면 `^[]T`(길이 있는 힙 버퍼)를 쓴다. `mem.alloc_slice(T, n)`가 반환. -- `str`은 별칭이 아니라 별개의 내장 타입이다. `[]u8`에서 `str`로는 `as str`로 변환할 수 있다(가변성 약화이므로 안전). `str`에서 `[]u8`로의 변환은 불변성을 깨므로 금지한다. `str`의 원소는 읽기만 가능하며 `s[i] = v`는 컴파일 에러다. 문자열 리터럴은 읽기 전용 저장 영역에 놓이므로 이 구분이 필요하다. +- 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. `let` 배열·공유 슬라이스에서는 `[]T`, `var` 배열·배타 슬라이스에서는 `[]mut T`가 생긴다. +- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 원래 대여 상태는 배타로 유지되며 다른 저장·대입에는 명시적 `as`가 필요하다. +- `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. +- `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 C 표현은 없다. ### 4.3 구조체 @@ -136,35 +139,37 @@ pub enum Shape { ```fe var p: ?^Node = null; if let Some(node) = p { node.value = 1; } // node: &mut Node (p가 mut일 때) -let v = p.?; // null이면 트랩 -let v = p orelse default_node; // null이면 우변 +p.?.value = 1; // projection chain, 소유권 이동 없음 +let v = mem.replace(&mut p, null).?; // 소유값을 실제로 꺼냄 ``` -- `?T`에서 T가 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용(크기 증가 없음). +- `?T`에서 T가 일반 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용한다. `?^[]T`는 빈 소유 버퍼와 null을 구별해야 하므로 `(bool, ^[]T)` 표현을 사용한다. - 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. +- `.?`, `.field`, `[i]`는 place projection이다. projection chain은 값을 이동하지 않는다. Copy 값은 읽기에서 복사되며 비-Copy 값을 projection에서 꺼내는 것은 R7에 따라 금지한다. 실제 추출은 `mem.replace`를 사용한다. +- `orelse`는 Copy payload를 복사한다. 비-Copy optional 변수 자체에 적용하면 optional 전체를 이동하며, field/index projection의 비-Copy optional에는 직접 적용할 수 없다. - `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. ### 4.6 에러 ```fe -pub error IoError { - NotFound = 1, - Denied = 2, - Eof = 3, +pub error ParseError { // 사용자 nominal error 예시 + InvalidDigit = 1, + Overflow = 2, } -fn read_all(path: str) -> IoError!^[]u8 { - let f = try io.open(path); // 에러면 즉시 반환 - defer f.close(); +fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 + var f = try io.open(path, io.Read); // 같은 core.Error면 즉시 반환 + defer { f.close() catch @trap(); } let n = f.size() catch |e| { return e; }; ... } ``` - `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. -- `E!T` 함수만 `try`/`catch` 사용 가능. -- `try e`: 에러면 현재 함수에서 즉시 반환(현재 함수도 에러 유니온을 반환해야 함). -- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) +- `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다. +- `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다. +- `try e`: 에러면 현재 함수에서 즉시 반환한다. +- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. @@ -174,11 +179,12 @@ fn read_all(path: str) -> IoError!^[]u8 { 멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. 최종 빌드에서 **드라이버가 emit 단계 이전에** 모든 유닛의 `.fei`에서 사용된 이름을 합치고 중복을 제거한 뒤 이름의 바이트순으로 정렬하여 1부터 `u16` 코드를 부여한다. +드라이버는 이 표를 단일 생성 헤더 `fe_errors.h`의 `#define` 정수 상수로 방출한다. 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 -결과가 결정적이며, 방출되는 C에서 **컴파일타임 정수 상수**이므로 `switch` case -라벨로 쓸 수 있다. -이름 집합이 바뀌면 코드가 재배치되므로 방출된 `.c`와 오브젝트 캐시는 전부 -무효화된다. `.fei` 자체는 코드가 아니라 이름만 기록하므로 무효화되지 않는다. +결과가 결정적이며 `switch` case 라벨로 쓸 수 있다. +이름 집합이 바뀌면 `fe_errors.h`와 그 헤더에 의존하는 오브젝트를 무효화하지만, +각 유닛의 `.c`는 재방출하지 않는다. `.fei`는 이름만 기록하므로 무효화되지 않는다. +빌드 디렉터리 이력에 따라 번호가 달라지는 append-only 표는 금지한다. 유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 확정된 표를 받아야 하며, 없으면 컴파일 에러다. 이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal @@ -187,12 +193,11 @@ fn read_all(path: str) -> IoError!^[]u8 { 축약형 `!T`를 반환하는 함수에서만 직접 반환할 수 있다. `--strip-error-names`를 사용하면 실행 파일과 런타임 오류 문자열에서 이름을 제거하지만 숫자 코드와 `.fei`의 타입/코드 일관성 정보는 유지한다. -`fmt.write_error`는 이 정책에 따라 `core.Error` 값을 이름 또는 코드로 출력한다. +`fmt.fmt_error`는 이 정책에 따라 `core.Error` 값을 이름 또는 코드로 포맷한다. -### 4.7 타입 동등성 +### 4.7 타입 동등성과 alias -이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. 별칭은 `const Alias = Type;`으로 만들며 완전 동일 취급. -`str`은 이 의미의 별칭이 아니라 `[]u8`과 구별되는 별개의 내장 타입이다(§4.2). +이름 기반(nominal). 필드가 같아도 다른 이름이면 다른 타입. `type` 값은 comptime 파라미터뿐 아니라 `const Alias = Type;`의 초기값에 허용하며, 이 선언은 새 nominal 타입이 아닌 완전 동일 alias를 만든다. 런타임 type 값은 없다. `str`은 이 규칙으로 미리 정의된 `[]u8` alias다(§4.2). --- @@ -202,11 +207,11 @@ fn read_all(path: str) -> IoError!^[]u8 { **R1 (단일 소유자).** 모든 값의 소유자는 정확히 하나. 변수 대입, 함수 인자 전달, 반환은 **이동(move)**이다. 이동된 변수는 이후 사용 시 컴파일 에러. -**R2 (Copy 타입).** 다음은 이동 대신 복사된다: 정수, `bool`, `char`, raw 포인터 `*T`, 참조 `&T`, 함수 포인터, 그리고 모든 필드가 Copy이면서 `drop`이 없는 struct/enum/배열. `^T`와 `&mut T`는 Copy가 아니다. +**R2 (Copy 타입).** 다음은 이동 대신 복사된다: 정수, `bool`, `char`, raw 포인터 `*T`, 공유 참조 `&T`, 공유 슬라이스 `[]T`(`str` 포함), 함수 포인터. `?T`, `E!T`, `[N]T`, struct/enum은 모든 포함 값이 Copy이고 `drop`이 없을 때 재귀적으로 Copy다. `^T`, `^[]T`, `&mut T`, `[]mut T`는 Copy가 아니다. -**R3 (소멸자, RAII).** `^T`는 소유자 스코프 종료 또는 재대입 시 `drop` 호출 후 해제. struct에 `fn drop(self: &mut Self)`가 있으면 그 값의 스코프 종료 시 자동 호출되며, 이어서 필드들의 drop이 선언 역순으로 호출된다. `drop`을 직접 호출하는 것은 컴파일 에러(`mem.destroy(x)` 사용). `^Self` 또는 `?^Self`를 재귀적으로 포함한 타입은 기본 필드 drop이 스택 깊이에 비례할 수 있으므로 컴파일러가 경고한다. 이런 연결 구조는 `mem.replace(&mut link, null)`로 소유 링크를 하나씩 꺼내 반복 해제하고 필드를 빈 값으로 남기는 사용자 `drop`을 정의해야 하며, `--deny-recursive-drop`으로 경고를 에러로 바꿀 수 있다. +**R3 (소멸자, RAII).** 일반 `^T`와 `^[]T`는 소유자 스코프 종료 또는 재대입 시 `drop` 호출 후 해제. struct에 `fn drop(self: &mut Self)`가 있으면 그 값의 스코프 종료 시 자동 호출되며, 이어서 필드들의 drop이 선언 역순으로 호출된다. `drop`을 직접 호출하는 것은 컴파일 에러(`mem.destroy(x)` 사용). `^Self` 또는 `?^Self`를 재귀적으로 포함한 타입은 기본 필드 drop이 스택 깊이에 비례할 수 있으므로 컴파일러가 경고한다. 이런 연결 구조는 `mem.replace(&mut link, null)`로 소유 링크를 하나씩 꺼내 반복 해제하고 필드를 빈 값으로 남기는 사용자 `drop`을 정의해야 하며, `--deny-recursive-drop`으로 경고를 에러로 바꿀 수 있다. -**R4 (참조는 2급 값).** `&T`, `&mut T`, `[]T`는 다음 위치에만 존재할 수 있다: +**R4 (참조는 2급 값).** `&T`, `&mut T`, `[]T`(`str` 포함), `[]mut T`는 다음 위치에만 존재할 수 있다: - 함수 파라미터 - 지역 변수 (`let`/`var`) - 표현식 안의 임시값 @@ -215,16 +220,18 @@ fn read_all(path: str) -> IoError!^[]u8 { - struct/enum 필드의 타입 - 배열/슬라이스의 원소 타입 - 함수 반환 타입 (예외: R8) -- `^T`, `*T`의 대상 타입 +- 일반 `^T`, `*T`의 대상 타입 - 전역 변수의 타입 -이 한 줄이 라이프타임 표기 전체를 불필요하게 만든다. +예외는 독립 소유 타입 `^[]T`/`?^[]T`와 문자열 리터럴로 초기화한 `const`/`static str`뿐이다. `^[]T`는 참조를 저장하지 않고 버퍼 자체를 소유한다. 이 제한이 라이프타임 표기 전체를 불필요하게 만든다. **R5 (참조 수명).** 지역 참조 변수는 대상보다 오래 살 수 없다. R4 덕분에 대상은 항상 같은 함수의 지역 변수, 파라미터, 또는 `static` 전역이므로 스코프 중첩 확인만으로 검사된다. 가변 전역에 대한 대여는 R10이 금지한다. **R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. -참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. +참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. `defer` 블록에서 사용한 참조와 그 원본의 대여는 해당 defer가 실행되는 스코프 끝까지 연장한다. + +호출 인자 위치의 `&mut T → &T`, `[]mut T → []T` 약화는 새 공유 대여가 아니라 기존 배타 대여의 읽기 전용 재대여다. 호출이 끝날 때 재대여만 끝나며 원래 배타 대여의 생존 구간은 유지된다. 이 판정은 함수 지역 liveness 분석이며 함수 밖 정보를 쓰지 않으므로 §1.2를 위반하지 않는다. @@ -235,36 +242,37 @@ r.^ = 1; // r의 마지막 사용 x += 1; // OK — 여기서 r의 대여는 이미 끝났다 ``` -**R7 (참조 무효화).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. +**R7 (참조 무효화와 부분 이동).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. own.c는 변수 단위 상태만 추적하므로 field/index/`.?` projection에서 비-Copy 소유값을 이동해 꺼내는 것은 금지한다. `mem.replace(&mut place, replacement)`로 유효한 대체값을 남기면서 꺼내야 한다. 배열의 선택적 소유 원소는 `?^T`로 두고 `mem.replace(&mut arr[i], null).?`로 꺼낸다. projection chain 자체(`p.?.^`, `s.field.x`)는 값을 소비하지 않는다. -**R8 (파생 반환).** 함수는 다음 두 경우에 한해 `&T`, `&mut T`, `[]T`, `str`을 반환할 수 있다. +**R8 (파생 반환).** 함수는 다음 두 경우에 한해 `&T`, `&mut T`, `[]T`, `[]mut T`와 이를 `?`로 감싼 타입을 반환할 수 있다. -**(a) 파라미터 파생.** 파라미터 중 참조성 타입(`&T`, `&mut T`, `[]T`, `str`)이 **정확히 하나**이고, 반환값이 그 파라미터에서 파생된 것임을 컴파일러가 함수 본문만 보고 확인할 수 있을 때. 파생이란 슬라이싱, 인덱싱, 필드 접근, `&`/`&mut` 취함, 그리고 다른 R8(a) 함수 호출의 연쇄를 말한다. 반환의 가변성은 원본 이하여야 한다(`&T`에서 `&mut T`를 만들 수 없다). 메서드의 `self`도 이 "하나"에 해당한다. +**(a) 파라미터 파생.** 메서드는 파생 원본이 항상 참조성 `self`여야 한다. 다른 참조성 인자를 추가로 받을 수 있지만 반환값은 그 인자에서 파생될 수 없고 그 인자의 임시 대여는 문장 끝에 풀린다. 자유 함수는 참조성 파라미터(`&T`, `&mut T`, `[]T`, `[]mut T`)가 **정확히 하나**여야 한다. 두 경우 모두 반환값이 정해진 원본에서 파생됐음을 컴파일러가 함수 본문만 보고 확인한다. 파생은 슬라이싱, 인덱싱, 필드 접근, projection, `&`/`&mut` 취함과 다른 R8(a) 호출의 연쇄다. 반환의 가변성은 원본 이하여야 한다. **(b) 정적 파생.** 반환값이 문자열 리터럴 또는 `static` 선언에서 파생된 경우. 이때는 참조성 파라미터가 없어도 된다. -호출 지점에서 R8(a)의 결과는 **그 인자를 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 인자 원본에 R6·R7이 그대로 적용된다. R8(b)의 결과는 대여를 만들지 않는다. +호출 지점에서 R8(a)의 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. R8(b)의 결과는 대여를 만들지 않는다. ```fe -pub fn trim(s: str) -> str { ... } // R8(a) -let t = str.trim(line); // OK. line은 t의 대여 구간 동안 잠긴다 +let t = line.trim(); // 내장 alias 메서드 R8(a) // OK. line은 t의 대여 구간 동안 잠긴다 -list.at(0).x = 5; // OK -let r = list.at(0); // OK. list를 &mut로 대여 +list.at_mut(0).x = 5; // &mut T, 배타 대여 +let r = list.at(0); // &T, 공유 대여 list.push(1); // 에러: r이 list를 대여 중 (R6) +map.get_str(key); // ?&V: self에서만 파생, key는 문장 끝에 해제 pub fn name() -> str { return "main"; } // R8(b) ``` -참조성 파라미터가 둘 이상인 함수는 어느 쪽에서 파생되었는지가 시그니처만으로 결정되지 않으므로 참조성 반환을 할 수 없다. 그런 함수가 필요하면 인덱스(`usize`)나 핸들을 반환한다. +자유 함수에 참조성 파라미터가 둘 이상이면 어느 쪽에서 파생됐는지 시그니처만으로 결정되지 않으므로 참조성 반환을 할 수 없다. 그런 함수가 필요하면 메서드로 만들어 `self`를 원본으로 고정하거나 인덱스(`usize`)·핸들을 반환한다. -**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `@as_far_fn`, `@call_far`, `asm`, `*_unchecked` 함수. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. +**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `@as_far_fn`, `@call_far`, `asm`, `*_unchecked` 함수. `*void`/`far *void`는 저장·비교·전달과 `@ptr_cast`에만 쓸 수 있고 직접 역참조할 수 없다. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. **R10 (전역과 인터럽트 공유).** `static`은 불변이며 컴파일타임 상수 초기화만 가능하다. 일반 전역 `var`의 읽기와 쓰기는 안전하며 `unsafe`가 필요 없다. 인터럽트 핸들러와 메인 흐름이 함께 접근하는 값은 반드시 `shared var`로 선언하고 다음 규칙을 적용한다. - 메인 흐름은 `critical {}` 안에서만 `shared var`에 접근할 수 있다. 진입 시 플래그를 저장하고 인터럽트를 막으며, 정상 종료·`return`·`break`·`continue`·에러 전파를 포함한 모든 이탈 경로에서 원래 플래그를 복원한다. - `interrupt fn` 안에서는 `shared var`에 직접 접근할 수 있다. 일반 함수와 `interrupt_safe fn`은 호출 문맥을 알 수 없으므로 명시적 `critical` 밖의 비원자 공유 접근이 금지된다. -- `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/포인터 스칼라에만 허용한다. 메인 흐름의 단일 읽기·쓰기에는 컴파일러가 필요한 최소 임계 구역을 생성한다. 복합 read-modify-write는 여전히 명시적 `critical {}`이 필요하다. 지원되지 않는 크기나 타입은 컴파일 에러다. -- `interrupt fn`은 `interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점, `critical` 및 안전하지 않은 함수 호출을 사용할 수 없으며 컴파일러가 함수 본문만 보고 검증한다. 이 효과는 `.fei` 시그니처에 기록한다. +- `shared var`와 `shared atomic var`는 C에서 `volatile T`로 방출한다. `critical` 진입/이탈에는 타깃 컴파일러용 memory barrier를 두어 접근이 경계 밖으로 이동하거나 병합되지 않게 한다. +- `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/near-pointer 스칼라에만 허용한다. 8086 인터럽트는 명령 경계에서만 진입하므로 bits16 단일 8/16비트 load/store는 `volatile` 한 명령으로 충분하며 자동 임계 구역을 만들지 않는다. far pointer와 복합 read-modify-write는 명시적 `critical {}`이 필요하다. +- `interrupt fn`은 `interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 `critical`, `@port_*`, `@volatile_*`, `asm`, 필요한 `unsafe`를 사용할 수 있다. 금지되는 것은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점 및 `interrupt_safe`가 아닌 함수 호출이다. 컴파일러가 본문만 보고 검증하고 이 효과를 `.fei` 시그니처에 기록한다. - 전역에 대한 대여는 다음으로 제한한다. `static`(불변)은 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 대여할 수 없으며 직접 읽기와 쓰기만 허용한다. `shared var`는 `critical` 안에서의 직접 접근만 허용하고 대여할 수 없다. 전역 값을 참조로 넘겨야 하면 지역 변수로 복사한 뒤 대여한다. - 이 제한의 근거는 R6다. 전역에 대한 대여가 살아 있는 동안 호출된 다른 함수가 같은 전역에 직접 접근할 수 있고, 그것은 함수 단위 지역 검사로 검출할 수 없다. 아래는 이 제한이 없으면 통과해 버리는 예다. @@ -289,15 +297,17 @@ unit := 'unit' ident ';' import* decl* import := 'import' ident ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl - | const_decl | global_decl) + | const_decl | global_decl) | comptime_decl +comptime_decl := 'comptime' 'if' expr '{' decl* '}' ['else' ('{' decl* '}' | comptime_decl)] fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type -struct_decl := ['packed'] 'struct' ident '{' member* '}' +generic_params := '(' ident (',' ident)* ')' +struct_decl := ['packed'] 'struct' ident [generic_params] '{' member* '}' member := ['pub'] (field | fn_decl) field := ident ':' type ',' -enum_decl := 'enum' ident '{' variant (',' variant)* [','] '}' +enum_decl := 'enum' ident [generic_params] '{' variant (',' variant)* [','] '}' variant := ident | ident '(' type ')' | ident '{' vfield* '}' vfield := ident ':' type ',' error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}' @@ -324,21 +334,22 @@ stmt := 'let' ident [':' type] '=' expr ';' if_stmt := 'if' (expr | 'let' pattern '=' expr) block ['else' (block | if_stmt)] while_stmt := 'while' expr block -for_stmt := 'for' ident [',' ident] 'in' expr block +for_stmt := 'for' ident [',' ident] 'in' for_source block +for_source := expr ['..' expr] match_stmt := 'match' expr '{' arm+ '}' arm := pattern '=>' (expr ';' | block) pattern := ident // 배리언트, 페이로드 없음 | ident '(' ident ')' // 튜플형 배리언트 바인딩 | ident '{' ident (',' ident)* '}' // 필드형 배리언트 바인딩 | 'Some' '(' ident ')' | 'None' - | int_literal | '_' + | int_literal | char_literal | 'true' | 'false' | '_' type := ident ['.' ident] | '?' type | '!' type | ident '!' type | '^' type | '&' ['mut'] type | '*' type | 'far' ('^' | '*' | '&' ['mut']) type | 'far' 'fn' '(' [type (',' type)*] ')' ['->' type] - | '[' expr ']' type | '[' ']' type + | '[' expr ']' type | '[' ']' ['mut'] type | 'fn' '(' [type (',' type)*] ')' ['->' type] | ident '(' type (',' type)* ')' // 제네릭 인스턴스 @@ -365,15 +376,18 @@ orelse_expr := expr 'orelse' expr 7 << >> 8 + - +% -% 9 * / % *% -10 단항: - not ~ & &mut ^(주소아님) try +10 단항: - not ~ & &mut try 11 후위: .field .? .^ [i] [a..b] (args) as T 12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) ``` - `and`, `or`는 단축 평가한다. 호스트 C 방출에서는 각각 `&&`, `||`로 매핑하며, 평가 순서와 단락 규칙은 Ferro 의미론을 그대로 유지한다. -- `as`는 후위 우선순위(단항보다 강함): `-x as i32`는 `-(x as i32)`. +- `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. +- `..`는 일반 표현식 연산자가 아니며 `for` 헤더에서만 쓸 수 있다. - 비교 연산 체이닝 금지(`a < b < c`는 에러). +- `.field`, `[i]`, `[a..b]`, 메서드 호출은 `&`, `&mut`, `^`를 필요한 만큼 자동 projection한다. 값 자체의 역참조는 `.^`가 필요하며 raw `*T`와 optional `?T`는 자동 역참조하지 않는다. +- `x.f(args)`는 메서드를 우선 탐색한다. 함수 포인터 필드를 호출하려면 `(x.f)(args)`로 쓴다. ### 6.3 빌트인 @@ -381,7 +395,7 @@ orelse_expr := expr 'orelse' expr @size_of(T) -> usize @align_of(T) -> usize @bits -> comptime int @target -> comptime str @ptr_cast(T, p) -> *T (unsafe) -@seg_ptr(seg: u16, off: u16) -> far *T (unsafe, bits16) +@seg_ptr(T, seg: u16, off: u16) -> far *T (unsafe, bits16) @port_in8(p) @port_in16(p) @port_out8(p,v) @port_out16(p,v) (unsafe) @volatile_load(p) @volatile_store(p, v) (unsafe) @trap() -> never @unreachable() -> never (unsafe) @@ -389,7 +403,7 @@ orelse_expr := expr 'orelse' expr @print(fmt, ...) -> void // stdout, 쓰기 오류 무시 @fprint(w, fmt, ...) -> !void // 임의 Writer -@sprint(buf: []u8, fmt, ...) -> usize // 버퍼에 기록, 쓴 바이트 수 반환 +@sprint(buf: []mut u8, fmt, ...) -> usize // 버퍼에 기록, 쓴 바이트 수 반환 @compile_error(msg) // comptime에서 항상 컴파일 에러 @as_far_fn(f) -> far fn() // bits16 전용 함수 포인터 변환 @call_far(p: far fn()) // bits16/unsafe 전용 호출 @@ -402,28 +416,29 @@ orelse_expr := expr 'orelse' expr ```fe @print("x={} y={x} name={s}\n", a, b, s); ``` -→ lower 단계에서 다음으로 전개: +→ lower 단계에서 개념적으로 다음처럼 전개한다: ``` -fmt.write_str(out, "x="); fmt.write_int_i32(out, a); -fmt.write_str(out, " y="); fmt.write_hex_u16(out, b); -fmt.write_str(out, " name="); fmt.write_str(out, s); -fmt.write_str(out, "\n"); +io.write(out, "x="); +var t1: [12]u8 = undefined; io.write(out, fmt.fmt_int_i32(t1[..], a)); +io.write(out, " y="); +var t2: [8]u8 = undefined; io.write(out, fmt.fmt_hex_u16(t2[..], b)); +io.write(out, " name="); io.write(out, s); io.write(out, "\n"); ``` +`fmt.fmt_*`는 `[]mut u8` 임시 버퍼에 쓰고 그 버퍼에서 파생된 `str`을 반환하는 순수 함수다. R8(a)의 원본이 하나이므로 별도 lifetime 표기가 필요 없다. 포매팅과 sink를 분리해 `@print`, `@fprint`, `@sprint`가 같은 변환 함수 한 벌을 사용한다. + 규칙: - 포맷 문자열은 **컴파일타임 문자열 리터럴 또는 `const`만**. 런타임 값이면 에러. - verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. - `{}` 개수와 인자 개수 불일치 → 컴파일 에러. -- 인자 타입에 대응하는 `fmt.write_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). -- 자릿수/폭/정렬 지정자는 v0.1.1에 없음. 필요하면 `fmt.write_int_pad`를 직접 호출. -- `@fprint`의 첫 인자는 `&mut io.Writer`(§10의 함수 포인터 struct). -- `@print`는 stdout에 기록하며 저수준 writer 오류를 삼키고 `void`를 반환한다. +- 인자 타입에 대응하는 `fmt.fmt_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). +- 자릿수/폭/정렬 지정자는 v0.1.1에 없음. 필요하면 `fmt.fmt_int_pad`를 직접 호출. +- `@fprint`의 첫 인자는 Copy 핸들 `io.Writer`(§10)다. +- `@print`는 `io.Writer.Stdout`에 기록하며 저수준 writer 오류를 삼키고 `void`를 반환한다. 따라서 `try @print(...)`는 컴파일 에러다. - `@fprint`는 writer 오류를 전파하여 `!void`를 반환한다. -- `@sprint`는 버퍼가 찬 뒤의 출력이 잘리더라도 트랩하지 않고 기록된 바이트 수를 - `usize`로 반환한다. -- 전개된 `fmt.write_*` 호출은 위 반환 규칙에 맞게 lower 단계에서 오류를 - 전파하거나 무시한다. `fmt.write_error`는 `core.Error`의 이름/코드를 출력한다. +- `@sprint`는 같은 `fmt.fmt_*` 결과를 대상 `[]mut u8`에 `mem.copy`로 이어 붙인다. 버퍼가 찬 뒤의 출력이 잘리더라도 트랩하지 않고 기록된 바이트 수를 `usize`로 반환한다. +- 전개된 `io.write` 호출은 위 반환 규칙에 맞게 lower 단계에서 오류를 전파하거나 무시한다. `fmt.fmt_error`는 `core.Error`의 이름/코드를 포맷한다. `@compile_error(msg)`의 `msg`는 comptime 문자열이어야 하며, 평가되는 분기에서 항상 진단을 발생시킨다. `comptime if`의 제거되는 분기에서는 진단하지 않는다. @@ -447,7 +462,7 @@ pub fn set_mode13() { pub fn put_pixel(x: u16, y: u16, c: u8) { if x >= WIDTH or y >= HEIGHT { return; } unsafe { - let vram: far *u8 = @seg_ptr(0xA000, 0); + let vram: far *u8 = @seg_ptr(u8, 0xA000, 0); @volatile_store(vram + (y * WIDTH + x) as usize, c); } } @@ -456,12 +471,10 @@ pub fn put_pixel(x: u16, y: u16, c: u8) { ```fe unit main; import io; -import list; -import fmt; fn count_lines(path: str) -> !usize { - let f = try io.open(path, io.Read); - defer f.close(); + var f = try io.open(path, io.Read); + defer { f.close() catch @trap(); } var buf: [256]u8 = undefined; var n: usize = 0; @@ -477,11 +490,10 @@ fn count_lines(path: str) -> !usize { pub fn main() -> !void { let n = count_lines("data.txt") catch |e| { - fmt.print_str("failed: "); - fmt.print_int(e as u16); + @print("failed: {}\n", e); return e; }; - fmt.print_int(n); + @print("{}\n", n); } ``` @@ -492,12 +504,13 @@ pub fn main() -> !void { ### 7.1 변수와 초기화 - `let`은 불변, `var`는 가변 선언이다. 두 형태 모두 초기값이 있으면 타입을 추론할 수 있다. `var x: T;`와 `var x: T = undefined;`처럼 초기값이 없거나 `undefined`이면 타입 명시가 필수다. +- `&mut x`, mutable slice 생성과 `&mut Self` 메서드 호출은 `var` place에서만 가능하다. `let`이 `^T`를 보유해도 그 대상을 안전 코드에서 변경할 수 없다. by-value `self: Self`는 소비 메서드 안에서 자신의 필드를 무효 상태로 바꿀 수 있는 가변 local owner로 취급한다. - 모든 변수는 사용 전 초기화 필수(정적 검사). 명시적 미초기화는 `= undefined`(unsafe 아님, 단 읽기 전 쓰기 필수는 여전히 검사). - 섀도잉 허용(같은 스코프에서 `let` 재선언). ### 7.2 제어 흐름 -- `for x in slice`: `x`는 `&T`(가변 슬라이스면 `&mut T`). 값 접근은 `x.^`. +- `for x in slice`: `x`는 `&T`(`[]mut T`이면 `&mut T`). 값 접근은 `x.^`. - `for i, x in slice`: `i: usize`. - `for i in a..b`: 정수 범위. - 이 루프 형태들은 경계 검사를 생략한다(컴파일러가 안전을 보장). @@ -522,7 +535,7 @@ pub fn main() -> !void { 트랩 발생 조건: 배열/슬라이스 경계 초과, 정수 오버플로, 0 나눗셈, `?T`의 `.?` 실패, `@trap()`. -동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. +동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 등록된 `sys.on_exit(fn)` 정리 함수를 역순 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. 일반 panic unwind나 defer 실행은 없지만 bits16 interrupt vector처럼 프로세스 종료 전에 반드시 복원할 자원은 allocation 없는 고정 크기 `on_exit` registry에 등록한다. `--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. **절대 제거되지 않는 것:** 소유권/참조 검사, 옵셔널 타입 검사, `match` 완전성 — 전부 컴파일타임이므로. @@ -542,7 +555,7 @@ pub fn main() -> !void { - `import bar;` → 같은 검색 경로의 `bar.fe`. 접근은 `bar.name`. - `pub` 붙은 선언만 외부 노출. 구조체 필드도 개별 `pub` 필요. - 순환 import 금지(에러). -- 유닛 컴파일 시 `.fei` 생성: pub 선언 시그니처, 타입 레이아웃, 제네릭 본문 토큰. 소스 해시가 같으면 재컴파일 생략. +- 유닛 컴파일 시 `.fei` 생성: pub 선언 시그니처, 타입 레이아웃, 제네릭 본문 토큰과 제네릭 전용 private 심볼 시그니처. 재컴파일 cache key는 해당 소스 해시뿐 아니라 직접·간접 의존 유닛의 `.fei` 해시를 포함한다. - 검색 경로: `-I `, 기본은 현재 디렉터리 + `/std`. ``` @@ -580,7 +593,8 @@ pub struct List(T) { pub fn new() -> List(T) { ... } pub fn push(self: &mut Self, v: T) -> !void { ... } - pub fn at(self: &Self, i: usize) -> &T { ... } // R8 적용 + pub fn at(self: &Self, i: usize) -> &T { ... } // R8 공유 + pub fn at_mut(self: &mut Self, i: usize) -> &mut T { ... } pub fn drop(self: &mut Self) { ... } } @@ -595,7 +609,9 @@ var xs: List(u8) = List(u8).new(); - 인스턴스화 시 타입 인자를 대입해 본문을 재검사하고 코드를 생성한다. 인스턴스 캐시 키는 `(선언, 타입 인자 목록)`. - 제약(trait bound) 없음. 본문에서 쓰는 연산이 그 타입에 없으면 **인스턴스화 시점에** 에러(에러 메시지에 인스턴스화 위치를 표시할 것). -- `.fei`에 제네릭 본문을 토큰 스트림으로 저장, 사용처에서 재파싱. +- 이름 해석은 항상 정의 유닛의 스코프에서 한다. `.fei`에 제네릭 본문 토큰과 본문이 참조하는 private 심볼의 제네릭 전용 시그니처를 저장한다. 이 표시는 Ferro source의 `pub` 접근 권한을 넓히지 않는다. +- 최종 build driver는 모든 사용 유닛의 인스턴스 요청을 합치고 중복 제거해 단일 `fe_generics.c`에 방출한다. 사용 유닛별 external 중복 심볼이나 `static` 코드 복제를 만들지 않는다. +- comptime에서 type 값의 `==`/`!=`, `@is_int(T)`, `@is_ptr(T)`를 허용한다. 타입 인터닝 identity로 평가하며 런타임 type reflection은 없다. - 재귀적 인스턴스화 깊이 제한 32. --- @@ -603,34 +619,21 @@ var xs: List(u8) = List(u8).new(); ## 10. 표준 라이브러리 (최소 집합) - **core**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. -- **mem**: `create(T) -> !^T`, `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst, src)`, `set(dst, v)`, `Arena{ init, alloc, reset, drop }`. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 재귀 구조의 반복 drop에도 사용한다. -- **str**: `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr(buf, s)`, `from_cstr(p)`. +- **mem**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다. +- **문자열/바이트**: `str`은 `[]u8` alias다. 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr`를 `line.trim()`처럼 호출하며 `str` 이름의 import 유닛은 두지 않는다. 소유 문자열 `String`은 `^[]u8`을 감싸고 `as_str(self: &Self) -> str`을 제공한다. - **list**: `List(T)`. -- **map**: `Map(K, V)` (오픈 어드레싱, `K`는 정수/str). -- **fmt**: `@print` 계열이 전개해 호출하는 저수준 함수 모음. - `write_str(w, s)`, `write_int_i8/i16/i32/u8/u16/u32(w, v)`, `write_hex_u8/u16/u32(w, v)`, - `write_char(w, c)`, `write_bool(w, b)`, `write_error(w, e)`, - `write_int_pad(w, v, width, pad)`. `write_error`는 `--strip-error-names` 설정을 - 따르며, 모든 함수는 `!void`를 반환한다. - 전부 `fn(w: &mut io.Writer, ...) -> !void` 시그니처. 사용자가 직접 호출해도 된다. +- **map**: `Map(K, V)`(오픈 어드레싱, K는 정수 또는 `String`). `String` key map은 key buffer를 소유하고 조회에는 `get_str(self: &Self, key: str) -> ?&V`를 제공한다. +- **fmt**: sink를 소유하지 않는 순수 변환 함수 모음. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error`는 `--strip-error-names`를 따른다. - **io**: - - `File{ open, create, read, write, seek, size, close(=drop) }`, `stdin`, `stdout`, `stderr`. - - `Writer` — 함수 포인터 struct (인터페이스 도입 전까지의 동적 디스패치 수단): - ```fe - pub struct Writer { - ctx: *void, - write_fn: fn(*void, []u8) -> !usize, - } - ``` - `File.writer(&mut self) -> Writer`, `buf_writer(buf: &mut []u8) -> Writer`, `null_writer()` 제공. - - `Reader` — 같은 형태, `read_fn: fn(*void, []u8) -> !usize`. - - `Writer`/`Reader`는 `*void`를 담으므로 필드 저장이 가능(2급 참조 아님). 대신 대상보다 오래 살면 - dangling이므로 **`Writer`를 만든 지역 스코프 밖으로 내보내지 않는 것**이 사용자 책임이며, - `writer()` 메서드는 R8(참조 반환) 대상이 아니라 값 반환이라 컴파일러가 막지 않는다. - v0.2에서 `dyn Writer`로 대체되면 이 구멍이 닫힌다. -- **sys**: `exit`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). - -`io.File`은 `drop`에서 핸들을 닫는다. 이중 닫기는 소유권 규칙이 막는다. + ```fe + pub enum Writer { Stdout, Stderr, File(u16), Null } + pub enum Reader { Stdin, File(u16) } + ``` + 둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다. `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다. 닫힌 fd 또는 재사용된 fd를 가진 복사 handle은 I/O 오류나 의도하지 않은 파일 접근이라는 논리 오류를 만들 수 있지만 dangling memory access는 만들지 않는다. + - `File{ open, create, read(self: &mut Self, []mut u8), write(self: &mut Self, []u8), seek, size, writer, reader, close }`. + - `close(self: Self) -> !void`는 File을 소비하는 일반 메서드이며 `drop`이 아니다. 내부 handle을 먼저 invalid 상태로 만든 뒤 닫기 오류를 반환하므로 함수 종료의 자동 drop은 no-op이다. `drop`은 아직 열린 handle만 오류를 무시하고 닫는다. `drop` 직접 호출 금지는 유지한다. + - 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 반환해서는 안 된다. 따라서 v0.1.2에는 함수 포인터/`*void` 기반 Writer·Reader나 buffer Writer가 없다. `@sprint`는 대상 slice에 직접 복사한다. +- **sys**: `exit`, `on_exit(f: fn() -> void) -> !void`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). `on_exit`은 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. --- @@ -670,7 +673,7 @@ fec/ lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개. emit_c.c/h LIR → C. §11.4 규칙. generic.c/h 인스턴스 캐시, 토큰 재파싱. - driver.c CLI, 유닛 의존 순서, .fei 캐시, 에러 이름 표 수집과 코드 부여(§4.6), + driver.c CLI, 유닛 의존 순서, .fei 캐시, fe_errors.h와 fe_generics.c 생성, 외부 C 컴파일러 호출. rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim std/ 표준 라이브러리 (.fe) @@ -687,16 +690,19 @@ fec/ | `i16`, `u32` 등 | `int16_t`, `uint32_t` (`` 없으면 자체 typedef) | | `usize` | `uint16_t`(bits16) / `uint32_t`(bits32) | | `bool` | `unsigned char` | -| `^T`, `*T` | `T*` | +| 일반 `^T`, `*T` | `T*` | +| `^[]T` | `typedef struct { T* p; fe_usize n; } fe_owned_slice_T;` | | `&T` | `const T*` | | `&mut T` | `T*` | | `far X` | `__far X` (Watcom/Borland), bits32는 무시 | | `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) | -| `[]T` | `typedef struct { T* p; fe_usize n; } fe_slice_T;` | -| `str` | `typedef struct { const uint8_t* p; fe_usize n; } fe_str;` | +| `[]T`/`str` | `typedef struct { const T* p; fe_usize n; } fe_slice_T;` | +| `[]mut T` | `typedef struct { T* p; fe_usize n; } fe_mut_slice_T;` | | `?T` (포인터류) | 원래 포인터, null 사용 | +| `?^[]T` | `struct { unsigned char has; fe_owned_slice_T v; }` | | `?T` (그 외) | `struct { unsigned char has; T v; }` | | `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | +| `shared [atomic] var x: T` | `volatile T x` | | struct | `struct fe__` | | enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` | | 함수 | `fe__`, 메서드는 `fe___` | @@ -709,15 +715,15 @@ fec/ - **`catch`**: `t.e`가 참일 때 블록 실행, 바인딩 변수는 `t.e`. - **`defer`/소멸자**: lower 단계에서 스코프 종료 지점(정상 흐름, `return`, `break`, `continue`, `try` 전파)마다 역순 호출을 명시적으로 삽입. C의 goto 라벨을 써도 되고 복제해도 된다(A는 복제, B는 goto 권장). - **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. -- **`match`**: `switch (x.tag)`. 페이로드 바인딩은 지역 변수로 복사 또는 포인터. +- **`match`**: `switch (x.tag)`. Copy payload는 지역 변수로 복사하고 non-Copy projection payload는 R7에 따라 참조로만 바인딩한다. 소유값 추출은 match 전 `mem.replace`로 수행한다. - **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. -- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 이미 `fmt.write_*` 호출 나열로 전개되므로 emit_c는 일반 함수 호출로만 본다. `@print`는 각 호출의 오류 코드를 명시적으로 버리고 `void`가 되며, `@fprint`는 첫 오류를 전파하고, `@sprint`는 남은 버퍼 길이를 추적해 잘라 쓴 뒤 실제 길이를 반환한다. 포맷 문자열 조각은 각각 static const 문자열 리터럴로 방출하고 동일 문자열은 중복 제거. +- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 `fmt.fmt_*`로 임시 `[]mut u8`에 변환하고 `io.write` 또는 `mem.copy`를 호출하는 나열로 전개한다. `@print`는 각 I/O 오류를 버리고, `@fprint`는 첫 오류를 전파하며, `@sprint`는 남은 길이를 추적해 잘라 쓴 실제 길이를 반환한다. 포맷 문자열 조각은 static const shared slice로 방출하고 동일 문자열은 중복 제거한다. - **참조와 aliasing**: `&T` → `const T*` 방출은 aliasing 가정을 하지 않는다. `&mut T`에도 `restrict`를 붙이지 않으며, M13/M14 네이티브 백엔드도 noalias를 가정하지 않는다. R6의 배타성은 R10의 전역 대여 금지가 함께 성립할 때만 프로그램 전체에서 유지되므로, 방출 단계에서 이를 최적화 근거로 쓰지 않는다. -- **에러 코드**: `error.Name`의 `u16` 코드는 드라이버가 emit 전에 확정한 정수 리터럴로 방출한다(§4.6). 따라서 에러 값에 대한 `match`도 `switch`로 방출할 수 있다. +- **에러 코드**: 드라이버가 emit 전에 확정한 `error.Name`의 `u16` 코드를 단일 `fe_errors.h`의 `#define`으로 방출한다(§4.6). 모든 유닛 C가 이 헤더를 include하므로 에러 `match`를 `switch`로 방출할 수 있고 이름 집합 변경 시 C 재방출 없이 오브젝트만 무효화한다. - **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. -- **임계 구역**: bits16의 `critical`은 진입 시 FLAGS를 저장한 뒤 `cli`하고 모든 이탈 경로에서 저장한 FLAGS를 복원한다. `shared atomic var`의 단일 접근도 같은 보존형 시퀀스를 사용하며 무조건 `sti`하지 않는다. -- **방출 순서**: typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문. -- 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성. +- **공유 상태와 임계 구역**: `shared`는 `volatile`로 방출한다. bits16의 `critical`은 compiler barrier → FLAGS 저장 → `cli` 순서로 진입하고 모든 이탈에서 저장한 FLAGS 복원 → compiler barrier 순서로 끝낸다. GCC 계열은 `asm volatile("" ::: "memory")`, Open Watcom/Borland는 optimizer가 내용을 볼 수 없는 별도 runtime 함수 호출 경계를 사용한다. 한 명령 크기의 `shared atomic` 단일 접근은 volatile load/store만 방출한다. +- **방출 순서**: `fe_errors.h` → typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문 → 통합 `fe_generics.c`. +- 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성한다. 제네릭 인스턴스 본문은 유닛 C에 중복 방출하지 않는다. ### 11.5 own.c 알고리즘 @@ -726,15 +732,16 @@ fec/ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive ``` -1. AST를 문장 순서로 순회하며 상태 전이. -2. 표현식 평가 시 lvalue 사용을 분류: 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기. -3. 이동: `Owned → Moved`. `Moved`/`MaybeMoved` 사용 시 에러(최초 이동 위치를 에러에 표시). -4. `&x`: `Owned → Shared(n+1)`. `&mut x`: `Owned → Exclusive`. 해제는 참조 변수의 스코프 끝(임시 참조는 문장 끝). -5. `Shared`/`Exclusive` 상태에서 쓰기/이동/재대여 시 에러(R6). -6. **분기 합류**: `if`/`match`의 각 브랜치를 독립 상태로 계산 후 병합. `Owned` + `Moved` → `MaybeMoved`(사용 에러, drop은 런타임 플래그). -7. **루프**: 본문을 2회 순회. 1회차 종료 상태를 진입 상태와 병합해 2회차 실행, 상태가 수렴하지 않으면(예: 첫 반복에서 이동) 에러. -8. R4 위반(참조를 필드/반환/힙에 저장)은 own이 아니라 check 단계에서 **타입만 보고** 거부한다. -9. R8(메서드 참조 반환)은 호출 결과를 바인딩하려는 시도를 check에서 거부. +1. 먼저 AST를 역방향 순회해 각 참조 변수의 경로별 마지막 사용을 계산한다. `defer` 안의 사용은 해당 스코프 끝으로 올린다. +2. AST를 문장 순서로 순회하며 상태 전이한다. 표현식의 place 사용을 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기 / projection으로 분류한다. +3. 이동: `Owned → Moved`. `Moved`/`MaybeMoved` 사용 시 에러(최초 이동 위치를 표시). field/index/`.?` projection의 비-Copy 이동은 R7에 따라 거부하고 `mem.replace`만 허용한다. +4. `&x`: `Owned → Shared(n+1)`. `&mut x`: `Owned → Exclusive`. 역방향 pass가 계산한 마지막 사용에서 해제하며 임시는 문장 끝에 해제한다. +5. 호출 인자의 `&mut → &`, `[]mut → []`는 Exclusive를 유지하는 임시 공유 view로 검사한다. +6. `Shared`/`Exclusive` 상태에서 금지된 쓰기/이동/재대여를 진단한다(R6). +7. **분기 합류**: `if`/`match`의 각 브랜치를 독립 상태로 계산 후 병합. `Owned` + `Moved` → `MaybeMoved`(사용 에러, drop은 런타임 플래그). +8. **루프**: 본문을 2회 순회. 1회차 종료 상태를 진입 상태와 병합해 2회차 실행, 상태가 수렴하지 않으면 에러. +9. R4 위반은 check 단계에서 타입만 보고 거부한다. 단 `static str`은 initializer가 문자열 리터럴인지 함께 확인한다. +10. R8 결과 바인딩을 허용하고 원본 대여를 결과의 마지막 사용까지 전파한다. 메서드는 self 파생, 자유 함수는 유일한 참조성 파라미터 파생인지 본문에서 확인한다. 에러 메시지 형식: `file:line:col: error: <설명>` + 관련 위치 `file:line:col: note: <최초 이동/대여 위치>`. @@ -744,13 +751,13 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive |---|---|---| | M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 | | M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 | -| M3 | struct, enum, match, 배열, 슬라이스, 경계 검사, str | 문자열 처리 예제 통과 | -| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), `io.Writer` 함수 포인터 struct | `@print`의 `void` 오류 삼킴, `@fprint`의 `!void` 전파, `@sprint`의 잘림/길이 동작과 인자 개수·타입 불일치가 컴파일 에러 | -| M5 | `^T`, drop, defer, 이동 검사 | 누수/이중해제 테스트 통과 | +| M3 | struct, enum, match, 배열, `[]T`/`[]mut T`, 경계 검사, `str` alias | 공유/배타 슬라이스와 문자열 처리 예제 통과 | +| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), handle enum `io.Writer`, 순수 `fmt.fmt_*` | `@print` 오류 삼킴, `@fprint` 전파, `@sprint` 잘림/길이와 인자 타입 진단, safe Writer dangling 불가 | +| M5 | `^T`/`^[]T`, drop, defer, 이동·부분 이동 검사 | 누수/이중해제, 소비 close, `mem.replace` 테스트 통과 | | M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | | M7 | `?T`, `E!T`, try/catch | io 유닛 동작 | -| M8 | 유닛/import/.fei, 분리 컴파일, std 초안 | 다중 유닛 프로그램 빌드 | -| M9 | **제네릭** (모노모피제이션) | `List(T)`, `Map(K,V)`를 Ferro로 재작성 | +| M8 | 유닛/import/.fei, 의존 hash, `fe_errors.h`, 분리 컴파일, std 초안 | 다중 유닛 증분·결정적 빌드 | +| M9 | **제네릭** (통합 모노모피제이션) | `fe_generics.c`로 `List(T)`, `Map(K,V)` 중복 없이 빌드 | | M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn, `shared`/`atomic`/`critical`/`interrupt_safe` | QEMU FreeDOS에서 자동화된 far 포인터·인터럽트 공유 상태 테스트 통과(VGA 데모는 수동/멀티모달 검증 대상이라 완료 게이트에서 제외) | | M11 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M10 테스트 통과 | | M12 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | @@ -760,7 +767,7 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive 배치 근거: - **M4(포매팅)를 앞에 두는 이유**: 구현이 작고(check + lower 합쳐 300줄 안팎) 언어 표면에 새 개념을 추가하지 않는다. 이후 모든 마일스톤의 디버깅과 M11의 컴파일러 B 에러 출력이 여기에 의존한다. - **M9(제네릭)가 M10보다 앞인 이유**: 제네릭 없이 표준 라이브러리를 쓰는 기간을 최소화한다. -- **인터페이스(`dyn`)는 마일스톤에 없다**: 부트스트랩 경로에 불필요하고(컴파일러 B는 인터페이스 없이 작성 가능), 타입 시스템 전반에 영향을 준다. §13의 v0.2 1순위로 미룬다. 그때까지 `io.Writer`/`io.Reader` 함수 포인터 struct로 대체한다. +- **인터페이스(`dyn`)는 마일스톤에 없다**: 부트스트랩 경로에 불필요하고 타입 시스템 전반에 영향을 준다. §13의 v0.2 1순위로 미룬다. 그때까지 dangling이 불가능한 Copy handle enum `io.Writer`/`io.Reader`를 사용한다. --- @@ -775,12 +782,12 @@ tests/ boot/ A/B 출력 비교, fixpoint 검증 ``` -- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 참조), R5(스코프 초과), R6(배타성 위반), R7(무효화), R8(참조성 파라미터 2개 이상에서 참조성 반환, 가변성 승격, 파생되지 않은 반환), R9(unsafe 밖 raw 역참조), R10(전역 `var` 대여), match 완전성, 암묵 변환, 타입 불일치, `str`을 `[]u8`로 변환, `str` 원소 쓰기. -- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `write_*` 없는 타입(예: struct), 닫히지 않은 `{`, `try @print(...)`(void). -- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print`의 오류 삼킴, `@sprint` 반환 길이/잘림 검증, `@fprint`를 `io.buf_writer`로 호출. -- R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `str.trim` 형태의 슬라이스 반환 연쇄. +- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 공유/배타 slice, `*[]T`), R5(스코프 초과), R6(배타성 위반), R7(무효화·projection 부분 이동), R8(자유 함수 참조성 파라미터 2개, 메서드의 non-self 파생, 가변성 승격), R9(unsafe 밖 raw 역참조, `*void` 역참조), R10(전역 `var` 대여), match 완전성, 암묵 변환, 타입 불일치, `let`에서 mutable slice 생성. +- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `fmt_*` 없는 타입, 닫히지 않은 `{`, `try @print(...)`(void). +- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print` 오류 삼킴, `@sprint` 길이/잘림, handle enum `@fprint`, 동일 `fmt.fmt_*` 결과 재사용. +- R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `line.trim()` 형태의 슬라이스 반환 연쇄. - `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 - `core.Error` 등록, 결정적 코드, `--strip-error-names`, `fmt.write_error`를 각각 pass/fail로 검증한다. + `core.Error` 등록, 결정적 `fe_errors.h`, `--strip-error-names`, `fmt.fmt_error`를 각각 pass/fail로 검증한다. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. - 회귀 실행: `make test` — 전 타깃 전 테스트. @@ -796,7 +803,8 @@ tests/ | 기능 | 등급 | 제외 이유 | 대체 수단 | |---|---|---|---| -| 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | 함수 포인터 struct (`io.Writer`, §10) | +| 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | Copy handle enum (`io.Writer`, §10) | +| bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 M10 범위 밖 | bits16 실행 파일 또는 polling | | 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 | | 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 | | 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | @@ -815,7 +823,7 @@ tests/ ### 13.1 인터페이스 설계 스케치 (v0.2 예정) -지금 구현하지 않되, 나중에 `io.Writer` 함수 포인터 struct를 무리 없이 대체할 수 있도록 방향만 고정해 둔다. +지금 구현하지 않되, 나중에 `io.Writer` Copy handle enum을 무리 없이 대체할 수 있도록 방향만 고정해 둔다. ```fe pub interface Writer { @@ -831,6 +839,6 @@ dump(&mut file, buf); // &mut File → &mut dyn Writer 자동 변환 - **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다. - `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱). - `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립 마일스톤으로 다룬다. -- 도입 시 `io.Writer`/`io.Reader`는 `dyn`으로 교체하고, 함수 포인터 struct 버전은 제거한다(`*void`가 만드는 dangling 구멍이 닫힌다). +- 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1.2의 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다. 위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토. From ec48a60e3edf311e6ec695be3f4f734230f37be7 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 17:42:43 +0900 Subject: [PATCH 033/184] feat: supervise EXEC with guest liveness instead of a stopwatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TCPAGENT는 system()이 도는 동안 통째로 얼어 있어 응답도 진행 보고도 못 한다. 그런데 호스트는 소켓에 30초 고정 타임아웃을 걸고, 만료되면 연결 자체를 버렸다. 그래서 35초짜리 컴파일이 "느린 명령"이 아니라 "죽은 에이전트"로 취급됐다. QEMU는 게스트가 얼어 있어도 계속 돈다. info blockstats의 idle_time_ns로 "작업 중"과 "멈춤"을 구분한다. 실측으로 확인했다: 에이전트가 완전히 벙어리인 동안에도 rd_operations가 7초당 47000씩 증가하고 idle은 0.00s를 유지한다. - EXEC은 짧은 간격으로 깨어나 감시만 하고 소켓은 절대 안 버린다. - --idle-timeout(기본 60s)과 --hard-timeout(기본 900s). 후자는 디스크를 안 쓰는 CPU 바운드 멈춤용 백스톱이다. - 중단은 QEMU 모니터로 Ctrl+C를 주입하고 COMMAND.COM의 "Terminate batch file (Y/N/A)?" 프롬프트에 답한다. - Ctrl+C는 DOS break check에서만 먹는다. FreeDOS 기본값 BREAK=OFF에서 출력을 파일로 돌린 CPU 바운드 자식은 거기 도달 안 할 수 있다. 그래서 중단은 보장이 아니라 요청으로 다루고, 명령이 안 멈춰도 RESULT를 끝까지 수거해 스트림을 깨뜨리지 않는다. - ferro-vm abort 추가. 실행 중에도 응답해야 하므로 파이프 서버를 요청당 스레드로 바꿨다. - 5558 바인딩을 SO_EXCLUSIVEADDRUSE로. Windows의 SO_REUSEADDR는 다른 프로세스가 같은 포트를 잡아 조용히 반쯤 동작하게 만든다. 검증 (QEMU FreeDOS 실측): - 32.4초 명령 정상 완료 (이전에는 30초에 실패) - 실행 중 abort가 0.1초에 응답, exit=95로 종료, 부분 출력 1805B 수거, 연결 유지 - pause처럼 디스크를 안 쓰는 명령을 idle 15s로 검출해 중단 시리얼 시절에 있다가 TCP 전환에서 사라진 TODO 3건을 복구한다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_012PQm6oAvWX4Lp3iSN5AHGT --- TODO.md | 28 ++++- src/ferrolang_vm/cli.py | 20 +++- src/ferrolang_vm/daemon.py | 228 +++++++++++++++++++++++++++++++------ tools/tcpagent/README.md | 25 +++- 4 files changed, 259 insertions(+), 42 deletions(-) diff --git a/TODO.md b/TODO.md index faeb710..d6fcdd1 100644 --- a/TODO.md +++ b/TODO.md @@ -1,5 +1,27 @@ # TODO -- [x] Add a non-reboot abort path for a hung DOS command: inject `Ctrl+C` through QEMU's monitor and wait for the serial agent to recover. -- [x] Apply a configurable timeout to `dos_exec` and invoke the non-reboot abort path on timeout. -- [x] Expose `dos_abort` for an immediate user-requested command interruption. +## Done + +- [x] Add a non-reboot abort path for a hung DOS command: inject `Ctrl+C` through + QEMU's monitor and wait for the agent to recover. +- [x] Apply a configurable timeout to `exec` and invoke the non-reboot abort path + on timeout. +- [x] Expose `abort` for an immediate user-requested command interruption. + +The three above were first built for the COM1 serial agent, lost in the rewrite +to the resident TCP agent, and rebuilt on the QEMU monitor in `ferro-vm exec`. +The TCP version supervises with guest disk liveness (`info blockstats`) rather +than a fixed stopwatch, so a slow compile is no longer mistaken for a hang. + +## Open + +- [ ] Consider `BREAK=ON` in `C:\FDCONFIG.SYS`. Ctrl+C only takes effect at a DOS + break check, and with the FreeDOS default of `BREAK=OFF` a compute-bound + child whose output is redirected to a file may never reach one, so `abort` + cannot always stop it. `BREAK=ON` checks on every DOS call and makes the + abort reliable, at a small cost to every DOS call. Needs a VM reboot; back + up `FDCONFIG.SYS` first. +- [ ] `TCPAGENT.EXE` connects from a fixed source port (`LOCAL_PORT 2058`). After + the host end closes, a reconnect reuses the same 4-tuple and can flap until + the old state ages out. Observed as a ~10s connect/disconnect cycle after + the daemon is killed mid-connection. diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py index 411b144..ff40ddb 100644 --- a/src/ferrolang_vm/cli.py +++ b/src/ferrolang_vm/cli.py @@ -70,6 +70,8 @@ EPILOG = r"""examples: uv run ferro-vm exec 'dir C:\FEC' run a DOS command, print exit code and output uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK + uv run ferro-vm exec --idle-timeout 180 'C:\FEC\BUILD-DOS.BAT' + uv run ferro-vm abort Ctrl+C the command running right now uv run ferro-vm logs follow the structured daemon log The authoritative workspace is C:\FEC inside the VM. Never build on D: (the vvfat @@ -84,6 +86,7 @@ SIMPLE_COMMANDS = { "screenshot": "Capture the VGA console to a PPM/PNG under .qemu/.", "ocr": "Capture the console and print recognized text (RapidOCR).", "logs": "Follow the append-only daemon log. Uses lnav when available.", + "abort": "Interrupt the DOS command currently running (Ctrl+C via QEMU).", } @@ -112,9 +115,19 @@ def main() -> int: execute = commands.add_parser( "exec", help=exec_help, description=exec_help + " Quote the command so the host shell does not eat" - r" backslashes: exec 'wcl386 -q HELLO.C'.") + r" backslashes: exec 'wcl386 -q HELLO.C'." + " A slow command is not a failed one: the wait ends" + " when the guest stops touching its disk, not when a" + " stopwatch expires.") execute.add_argument("command", metavar="DOS_COMMAND", help=r"command line to hand to COMMAND.COM, e.g. 'dir C:\FEC'") + execute.add_argument("--idle-timeout", type=float, default=60, metavar="SECONDS", + help="interrupt once the guest has made no disk access for" + " this long (default: %(default)s)") + execute.add_argument("--hard-timeout", type=float, default=900, metavar="SECONDS", + help="interrupt after this much total time regardless of" + " activity; the backstop for a CPU-bound hang" + " (default: %(default)s)") put_help = "Copy a host file into the VM." put = commands.add_parser("put", help=put_help, description=put_help) @@ -161,7 +174,10 @@ def main() -> int: return 0 raise RuntimeError("TCPAGENT did not become ready") payload: dict[str, object] = {"op": args.op} - if args.op == "exec": payload["command"] = args.command + if args.op == "exec": + payload["command"] = args.command + payload["idle_timeout"] = args.idle_timeout + payload["hard_timeout"] = args.hard_timeout if args.op == "put": payload["source"] = str(args.source.resolve()) payload["destination"] = args.destination diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py index e35bdf2..cbe9219 100644 --- a/src/ferrolang_vm/daemon.py +++ b/src/ferrolang_vm/daemon.py @@ -8,6 +8,7 @@ from __future__ import annotations import json import logging import os +import re import select import shutil import socket @@ -26,6 +27,22 @@ AGENT_ADDRESS = ("127.0.0.1", 5558) MONITOR_ADDRESS = ("127.0.0.1", 4444) LOG_PATH = QEMU / "ferro-vm.log" +# Short commands answer promptly, so a plain socket timeout is the right guard. +REQUEST_TIMEOUT = 30 +# EXEC is different: TCPAGENT is frozen inside system() for the whole command +# and cannot answer, so silence proves nothing. Wake up often, decide with +# guest liveness instead of a stopwatch, and never discard the connection just +# because a compile is slow. +EXEC_POLL_SECONDS = 2 +DEFAULT_IDLE_TIMEOUT = 60 +DEFAULT_HARD_TIMEOUT = 900 +# Budget for collecting the result after Ctrl+C, before giving up on the stream. +INTERRUPT_GRACE_SECONDS = 15 + + +class ExecInterrupted(RuntimeError): + """The supervisor decided to stop the running DOS command.""" + def configure_logging() -> None: QEMU.mkdir(exist_ok=True) @@ -46,16 +63,37 @@ class Host: self.agent_lock = threading.Lock() self.agent_ready = threading.Event() self.qemu: subprocess.Popen[bytes] | None = None + # QEMU accepts one monitor connection at a time, and the EXEC + # supervisor polls it while other control requests run concurrently. + self.monitor_lock = threading.Lock() + self.abort_requested = threading.Event() + self.exec_active = threading.Event() - def accept_agents(self) -> None: + @staticmethod + def bind_agent_listener() -> socket.socket: + """Bind 5558 exclusively so a second daemon fails loudly. + + SO_REUSEADDR means something different on Windows than on Unix: it lets + another process bind an already-bound port and quietly take over new + connections, so a duplicate daemon would be silently half-working + instead of refusing to start. SO_EXCLUSIVEADDRUSE is the Windows way to + say "only me". + """ server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + exclusive = getattr(socket, "SO_EXCLUSIVEADDRUSE", None) + if exclusive is not None: + server.setsockopt(socket.SOL_SOCKET, exclusive, 1) + else: + server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind(AGENT_ADDRESS) server.listen(1) + return server + + def accept_agents(self, server: socket.socket) -> None: log_event(logging.INFO, "agent listener ready", address="127.0.0.1:5558") while True: sock, peer = server.accept() - sock.settimeout(30) + sock.settimeout(REQUEST_TIMEOUT) with self.agent_lock: if self.agent is not None: sock.close() @@ -94,15 +132,62 @@ class Host: log_event(logging.INFO, "agent disconnected", peer=f"{peer[0]}:{peer[1]}") @staticmethod - def _read_line(sock: socket.socket) -> bytes: + def _read_line(sock: socket.socket, supervise=None) -> bytes: + # Partial input is kept across timeouts, so supervise() may fire in the + # middle of a line without losing what has already arrived. out = bytearray() while not out.endswith(b"\n"): - part = sock.recv(1) + try: + part = sock.recv(1) + except TimeoutError: + if supervise is None: + raise + supervise() + continue if not part: raise ConnectionError("TCP agent closed connection") out.extend(part) return bytes(out).rstrip(b"\r\n") + @staticmethod + def _read_exactly(sock: socket.socket, count: int, supervise=None) -> bytes: + chunks: list[bytes] = [] + while count: + try: + chunk = sock.recv(min(65536, count)) + except TimeoutError: + if supervise is None: + raise + supervise() + continue + if not chunk: + raise ConnectionError("TCP agent closed connection") + chunks.append(chunk) + count -= len(chunk) + return b"".join(chunks) + + def guest_idle_seconds(self) -> float | None: + """Seconds since the guest last touched a disk, or None if unknown. + + QEMU keeps counting while TCPAGENT is frozen inside system(), so this + is the one progress signal available during a long DOS command. A + purely CPU-bound command looks idle here, which is what the hard + timeout is for. + """ + try: + text = self.monitor("info blockstats") + except OSError: + return None + idle: float | None = None + for line in text.splitlines(): + operations = re.search(r"rd_operations=(\d+)", line) + elapsed = re.search(r"idle_time_ns=(\d+)", line) + if not operations or not elapsed or int(operations.group(1)) == 0: + continue + seconds = int(elapsed.group(1)) / 1e9 + idle = seconds if idle is None else min(idle, seconds) + return idle + def request(self, command: str, payload: bytes = b"") -> str: with self.agent_lock: if self.agent is None: @@ -125,45 +210,107 @@ class Host: def ping(self) -> dict[str, object]: return {"response": self.request("PING")} - def exec(self, command: str) -> dict[str, object]: - log_event(logging.INFO, "exec start", command=command) + def _read_exec_result(self, sock: socket.socket, supervise=None) -> tuple[int, int, bytes]: + header = self._read_line(sock, supervise).decode("ascii", "replace") + fields = header.split() + if len(fields) == 4 and fields[0] == "RESULT": + code, length, flags = int(fields[1]), int(fields[2]), int(fields[3]) + return code, flags, self._read_exactly(sock, length, supervise) + if fields and fields[0] == "OK": + # Compatibility with an installed pre-RESULT agent. + return int(fields[1]), 1, bytes.fromhex(fields[2]) if len(fields) > 2 else b"" + raise RuntimeError("malformed EXEC response: " + header) + + def exec(self, command: str, idle_timeout: float = DEFAULT_IDLE_TIMEOUT, + hard_timeout: float = DEFAULT_HARD_TIMEOUT) -> dict[str, object]: + log_event(logging.INFO, "exec start", command=command, + idle_timeout=idle_timeout, hard_timeout=hard_timeout) started = time.monotonic() + self.abort_requested.clear() with self.agent_lock: if self.agent is None: raise RuntimeError("TCPAGENT is not connected") sock = self.agent + self.exec_active.set() + reported = started + interrupt_at: float | None = None + interrupted = "" + answered = False + + def supervise() -> None: + """Called every EXEC_POLL_SECONDS while the agent stays silent.""" + nonlocal reported, interrupt_at, interrupted, answered + now = time.monotonic() + idle = self.guest_idle_seconds() + if now - reported >= 15: + reported = now + log_event(logging.INFO, "exec running", elapsed_s=round(now-started, 1), + guest_idle_s=None if idle is None else round(idle, 1)) + if interrupt_at is None: + if self.abort_requested.is_set(): + interrupted = "aborted by request" + elif now - started > hard_timeout: + interrupted = f"hard timeout after {hard_timeout:.0f}s" + elif idle is not None and idle > idle_timeout: + interrupted = f"guest idle {idle:.0f}s exceeds {idle_timeout:.0f}s" + if interrupted: + interrupt_at = now + log_event(logging.WARNING, "exec interrupting", reason=interrupted) + self.monitor("sendkey ctrl-c") + return + waited = now - interrupt_at + if not answered and waited > 4: + # COMMAND.COM asks "Terminate batch file (Y/N/A)?" for .BAT + # targets and sits at that prompt until it is answered. + answered = True + self.monitor("sendkey y") + self.monitor("sendkey ret") + # Ctrl+C only lands at a DOS break check. With BREAK=OFF (the + # FreeDOS default) a compute-bound child whose output we + # redirected to a file may never reach one, so the command runs + # to completion regardless. Keep collecting its result rather + # than abandoning a stream that still owes us one -- give up + # only once the guest has gone quiet too. + if waited > INTERRUPT_GRACE_SECONDS and (idle is None or idle > 5): + raise ExecInterrupted(interrupted + "; command did not stop") + try: encoded = command.encode("ascii", "replace").hex().upper() + sock.settimeout(EXEC_POLL_SECONDS) sock.sendall(f"EXEC {encoded}\n".encode("ascii")) - header = self._read_line(sock).decode("ascii", "replace") - fields = header.split() - if len(fields) == 4 and fields[0] == "RESULT": - code, remaining, flags = int(fields[1]), int(fields[2]), int(fields[3]) - chunks: list[bytes] = [] - while remaining: - chunk = sock.recv(min(65536, remaining)) - if not chunk: - raise ConnectionError("TCP agent closed during EXEC result") - chunks.append(chunk) - remaining -= len(chunk) - raw = b"".join(chunks) - elif fields and fields[0] == "OK": - # Compatibility with an installed pre-RESULT agent. - code = int(fields[1]); flags = 1 - raw = bytes.fromhex(fields[2]) if len(fields) > 2 else b"" - else: - raise RuntimeError("malformed EXEC response: " + header) - except OSError as exc: + code, flags, raw = self._read_exec_result(sock, supervise) + except (OSError, ExecInterrupted) as exc: + # Only now is the stream beyond repair; drop it so the agent + # reconnects with a clean protocol state. if self.agent is sock: self.agent = None self.agent_ready.clear() + sock.close() raise RuntimeError(f"TCPAGENT EXEC failed: {exc}") from exc + finally: + self.exec_active.clear() + self.abort_requested.clear() + try: + sock.settimeout(REQUEST_TIMEOUT) + except OSError: + pass output = raw.decode("cp437", "replace") for line in output.splitlines(): log_event(logging.INFO, "dos output", line=line) log_event(logging.INFO, "exec finish", exit=code, bytes=len(raw), flags=flags, + interrupted=interrupted or None, elapsed_ms=round((time.monotonic()-started)*1000)) - return {"exit": code, "output": output, "bytes": len(raw), "flags": flags} + result = {"exit": code, "output": output, "bytes": len(raw), "flags": flags} + if interrupted: + result["interrupted"] = interrupted + return result + + def abort(self) -> dict[str, object]: + if not self.exec_active.is_set(): + return {"aborted": False, "reason": "no command is running"} + self.abort_requested.set() + log_event(logging.INFO, "abort requested") + return {"aborted": True} def put(self, source: str, destination: str) -> dict[str, object]: data = Path(source).read_bytes() @@ -198,9 +345,8 @@ class Host: log_event(logging.INFO, "get", path=source, bytes=len(data), destination=str(target)) return {"path": source, "destination": str(target), "bytes": len(data)} - @staticmethod - def monitor(command: str) -> str: - with socket.create_connection(MONITOR_ADDRESS, timeout=3) as sock: + def monitor(self, command: str) -> str: + with self.monitor_lock, socket.create_connection(MONITOR_ADDRESS, timeout=3) as sock: sock.settimeout(1) time.sleep(.1) try: @@ -269,7 +415,11 @@ class Host: if op == "start": return self.start() if op == "stop": return self.stop() if op == "ping": return self.ping() - if op == "exec": return self.exec(str(request["command"])) + if op == "abort": return self.abort() + if op == "exec": + return self.exec(str(request["command"]), + float(request.get("idle_timeout", DEFAULT_IDLE_TIMEOUT)), + float(request.get("hard_timeout", DEFAULT_HARD_TIMEOUT))) if op == "put": return self.put(str(request["source"]), str(request["destination"])) if op == "get": return self.get(str(request["source"]), str(request["destination"])) if op == "screenshot": return self.screenshot() @@ -280,8 +430,8 @@ class Host: def serve_pipe(host: Host) -> None: listener = Listener(PIPE, family="AF_PIPE") log_event(logging.INFO, "control pipe ready", pipe=PIPE) - while True: - conn = listener.accept() + + def handle(conn) -> None: try: request = conn.recv() try: @@ -292,13 +442,23 @@ def serve_pipe(host: Host) -> None: finally: conn.close() + while True: + # One thread per request: `abort` has to be answerable while a long + # `exec` is still holding the agent. + threading.Thread(target=handle, args=(listener.accept(),), daemon=True).start() + def main() -> None: if os.name != "nt": raise SystemExit("ferro-vm currently supports Windows only") configure_logging() host = Host() - threading.Thread(target=host.accept_agents, daemon=True).start() + try: + server = host.bind_agent_listener() + except OSError as exc: + log_event(logging.ERROR, "agent listener bind failed", address="127.0.0.1:5558", error=str(exc)) + raise SystemExit(f"another ferro-vm daemon already owns 127.0.0.1:5558 ({exc})") + threading.Thread(target=host.accept_agents, args=(server,), daemon=True).start() serve_pipe(host) diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md index 0b2487f..249b8c8 100644 --- a/tools/tcpagent/README.md +++ b/tools/tcpagent/README.md @@ -61,9 +61,28 @@ are not interpreted on this FreeDOS console, so neither of the usual routes works. Elapsed times come from the BIOS tick counter at 18.2065 Hz (~55 ms resolution). -Note that mTCP is not driven while `system()` runs a child, so a DOS command -lasting tens of seconds can drop the TCP connection. The agent logs `link lost` -and reconnects on its own, but the host loses that command's result. +## Long commands + +mTCP is only driven when the agent calls it, and `system()` freezes the agent +for the entire child command. So during a long `EXEC` the DOS side is mute: it +cannot answer, cannot acknowledge, cannot report progress. Silence therefore +proves nothing about whether the command is healthy. + +The host must not read that silence as failure. `ferro-vm exec` waits on +QEMU's own view of the guest instead: `info blockstats` keeps counting while +the agent is frozen, and `idle_time_ns` distinguishes a slow command from a +stuck one. See `--idle-timeout` and `--hard-timeout` in `ferro-vm exec --help`. + +When the host does decide to stop a command it injects Ctrl+C through the QEMU +monitor, then answers COMMAND.COM's `Terminate batch file (Y/N/A)?` prompt. +That is a request, not a guarantee: Ctrl+C only lands at a DOS break check, and +with `BREAK=OFF` (the FreeDOS default in `C:\FDCONFIG.SYS`) a compute-bound +child whose output we redirected to a file may never reach one. The host keeps +collecting the result either way rather than abandoning a stream that still +owes it a `RESULT`. + +Adding `BREAK=ON` to `C:\FDCONFIG.SYS` would make DOS check on every system +call and so make Ctrl+C reliable, at a small cost to every DOS call. ## Rebuilding inside the VM From cba2c86fbc2815004729adcf7a8a38d8cf5d8419 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 18:11:52 +0900 Subject: [PATCH 034/184] fix: reliably answer delayed DOS batch termination prompts --- src/ferrolang_vm/daemon.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py index cbe9219..20b3e9d 100644 --- a/src/ferrolang_vm/daemon.py +++ b/src/ferrolang_vm/daemon.py @@ -235,11 +235,10 @@ class Host: reported = started interrupt_at: float | None = None interrupted = "" - answered = False def supervise() -> None: """Called every EXEC_POLL_SECONDS while the agent stays silent.""" - nonlocal reported, interrupt_at, interrupted, answered + nonlocal reported, interrupt_at, interrupted now = time.monotonic() idle = self.guest_idle_seconds() if now - reported >= 15: @@ -259,16 +258,18 @@ class Host: self.monitor("sendkey ctrl-c") return waited = now - interrupt_at - if not answered and waited > 4: - # COMMAND.COM asks "Terminate batch file (Y/N/A)?" for .BAT - # targets and sits at that prompt until it is answered. - answered = True - self.monitor("sendkey y") - self.monitor("sendkey ret") - # Ctrl+C only lands at a DOS break check. With BREAK=OFF (the - # FreeDOS default) a compute-bound child whose output we - # redirected to a file may never reach one, so the command runs - # to completion regardless. Keep collecting its result rather + # DOS answers Ctrl+C with "Terminate batch file (Y/N/A)?" and + # waits there. The prompt only appears once COMMAND.COM reaches + # the next batch line, which can be many seconds into a slow + # command, so answer on every poll rather than once: a single + # early 'y' is swallowed by whatever is still running. Send only + # 'y' -- the prompt takes one keystroke, and a trailing Enter + # gets read as "keep going". + self.monitor("sendkey y") + # Even answered, Ctrl+C is a request. It lands only at a DOS + # break check, and a DOS/4GW child (wcc386, wmake) runs in + # protected mode where it may never reach one, so the command + # can still run to completion. Keep collecting its result rather # than abandoning a stream that still owes us one -- give up # only once the guest has gone quiet too. if waited > INTERRUPT_GRACE_SECONDS and (idle is None or idle > 5): From 42abc0d7e77812a62974a553fbc0d4cfaca2df26 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 18:23:30 +0900 Subject: [PATCH 035/184] feat: align M3 slices and strings with v0.1.7 --- fec/src/ast.c | 2 +- fec/src/ast.h | 1 + fec/src/check.c | 53 ++++++++++----- fec/src/emit_c.c | 68 +++++++++++++------ fec/src/parser.c | 1 + fec/src/types.c | 32 +++++++-- fec/src/types.h | 1 + fec/test-dos.bat | 56 +++++++++------ .../fail/{logical-symbols.fe => logical.fe} | 0 .../fail/{missing-semi.fe => misssemi.fe} | 0 .../fail/{unclosed-comment.fe => unclcomm.fe} | 0 fec/tests/m2/{bad-arity.fe => bad-ari.fe} | 0 fec/tests/m2/{bad-assign.fe => bad-asgn.fe} | 0 .../m2/{bad-condition.fe => bad-cond.fe} | 0 fec/tests/m2/{bad-return.fe => bad-ret.fe} | 0 fec/tests/m2/{bad-types.fe => bad-type.fe} | 0 fec/tests/m2/{bad-uninit.fe => bad-unit.fe} | 0 fec/tests/m2/{bad-unknown.fe => bad-unk.fe} | 0 fec/tests/m2/{cast-while.fe => castwhil.fe} | 0 fec/tests/m3/bad-mlet.fe | 6 ++ fec/tests/m3/bad-shwr.fe | 5 ++ fec/tests/m3/mutable.fe | 11 +++ fec/tests/m3/slcbound.fe | 7 ++ fec/tests/m4/{bad-arity.fe => bad-ari.fe} | 0 fec/tests/m4/{bad-runtime.fe => bad-run.fe} | 0 fec/tests/m4/{bad-writer.fe => bad-writ.fe} | 0 fec/tests/m4/{try-fprint.fe => try-fpr.fe} | 0 .../m5/{bad-conditional.fe => bad-cond.fe} | 0 fec/tests/m5/{bad-double.fe => bad-dbl.fe} | 0 fec/tests/m5/{bad-destroy.fe => bad-dest.fe} | 0 .../{keywords-and-builtins.fe => keybuilt.fe} | 0 fec/tests/pass/{v012-forms.fe => v012form.fe} | 0 32 files changed, 183 insertions(+), 60 deletions(-) rename fec/tests/fail/{logical-symbols.fe => logical.fe} (100%) rename fec/tests/fail/{missing-semi.fe => misssemi.fe} (100%) rename fec/tests/fail/{unclosed-comment.fe => unclcomm.fe} (100%) rename fec/tests/m2/{bad-arity.fe => bad-ari.fe} (100%) rename fec/tests/m2/{bad-assign.fe => bad-asgn.fe} (100%) rename fec/tests/m2/{bad-condition.fe => bad-cond.fe} (100%) rename fec/tests/m2/{bad-return.fe => bad-ret.fe} (100%) rename fec/tests/m2/{bad-types.fe => bad-type.fe} (100%) rename fec/tests/m2/{bad-uninit.fe => bad-unit.fe} (100%) rename fec/tests/m2/{bad-unknown.fe => bad-unk.fe} (100%) rename fec/tests/m2/{cast-while.fe => castwhil.fe} (100%) create mode 100644 fec/tests/m3/bad-mlet.fe create mode 100644 fec/tests/m3/bad-shwr.fe create mode 100644 fec/tests/m3/mutable.fe create mode 100644 fec/tests/m3/slcbound.fe rename fec/tests/m4/{bad-arity.fe => bad-ari.fe} (100%) rename fec/tests/m4/{bad-runtime.fe => bad-run.fe} (100%) rename fec/tests/m4/{bad-writer.fe => bad-writ.fe} (100%) rename fec/tests/m4/{try-fprint.fe => try-fpr.fe} (100%) rename fec/tests/m5/{bad-conditional.fe => bad-cond.fe} (100%) rename fec/tests/m5/{bad-double.fe => bad-dbl.fe} (100%) rename fec/tests/m5/{bad-destroy.fe => bad-dest.fe} (100%) rename fec/tests/pass/{keywords-and-builtins.fe => keybuilt.fe} (100%) rename fec/tests/pass/{v012-forms.fe => v012form.fe} (100%) diff --git a/fec/src/ast.c b/fec/src/ast.c index 63fdff7..561b834 100644 --- a/fec/src/ast.c +++ b/fec/src/ast.c @@ -8,7 +8,7 @@ FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned lo FeNode *n=(FeNode *)fe_arena_alloc(&a->arena,sizeof(FeNode)); if (!n) return 0; n->kind=k; n->loc=loc; n->text=text?fe_arena_strdup(&a->arena,text,len):0; - n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->aux_text=0; n->aux_cname=0; n->sem_type=0; n->flags=0; return n; + n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->aux_text=0; n->aux_cname=0; n->sem_type=0; n->sem_decl=0; n->flags=0; return n; } void fe_node_add(FeNode *parent, FeNode *child) { diff --git a/fec/src/ast.h b/fec/src/ast.h index 33140e4..02b3743 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -30,6 +30,7 @@ struct FeNode { char *aux_text; char *aux_cname; FeType *sem_type; + FeNode *sem_decl; unsigned flags; }; diff --git a/fec/src/check.c b/fec/src/check.c index 912936c..4bda576 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -54,7 +54,7 @@ static int is_copy_type(FeType *t) unsigned i; if (!t) return 1; if (t->kind==FE_TYPE_OWNED) return 0; - if (t->kind==FE_TYPE_REF) return !t->ref_mut; + if (t->kind==FE_TYPE_REF || t->kind==FE_TYPE_SLICE) return !t->ref_mut; if (t->kind==FE_TYPE_ARRAY) return is_copy_type(t->elem); if (t->kind==FE_TYPE_STRUCT) { if (t->has_drop) return 0; @@ -311,6 +311,8 @@ static FeNode *find_const_node(FeCheck *c, const char *name) return 0; } +static int format_is_slice_u8(FeType *t); + static const char *builtin_format(FeCheckerState *s, FeNode *fmt) { FeNode *decl; @@ -323,7 +325,7 @@ static const char *builtin_format(FeCheckerState *s, FeNode *fmt) sym->decl : find_const_node(s->c,fmt->text); if (decl && decl->b && decl->b->kind==FE_N_LITERAL && decl->b->text && decl->b->text[0]=='"') { - if (!decl->a || fe_type_from_ast(&s->c->types,decl->a)->kind==FE_TYPE_STR) + if (!decl->a || format_is_slice_u8(fe_type_from_ast(&s->c->types,decl->a))) return decl->b->text; } } @@ -347,10 +349,10 @@ static int format_arg_ok(FeType *t, int verb) if (!t) return 0; if (verb=='x') return fe_type_is_integer(t); if (verb=='c') return t->kind==FE_TYPE_CHAR; - if (verb=='s') return t->kind==FE_TYPE_STR || format_is_slice_u8(t); + if (verb=='s') return format_is_slice_u8(t); if (verb=='b') return t->kind==FE_TYPE_BOOL; if (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_BOOL || - t->kind==FE_TYPE_CHAR || t->kind==FE_TYPE_STR) return 1; + t->kind==FE_TYPE_CHAR) return 1; return format_is_slice_u8(t) || (t->kind==FE_TYPE_ENUM && t->is_error); } @@ -381,7 +383,8 @@ static void check_format_call(FeCheckerState *s, FeNode *n) if (strcmp(n->text,"@sprint")==0) { if (!fmt_node) { err(s->c,n->loc,"@sprint requires a buffer"); return; } t=check_expr(s,fmt_node); - if (!format_is_slice_u8(t)) err(s->c,fmt_node->loc,"@sprint requires []u8 buffer"); + if (!format_is_slice_u8(t) || !t->ref_mut) + err(s->c,fmt_node->loc,"@sprint requires []mut u8 buffer"); fmt_node=fmt_node->next; } fmt=builtin_format(s,fmt_node); @@ -517,7 +520,18 @@ static FeType *check_index(FeCheckerState *s, FeNode *n) if(n->c || !n->b) { if (base->kind==FE_TYPE_ARRAY && !array_slice_lvalue(n->a)) err(s->c,n->loc,"array slicing requires a stable lvalue"); - if(n->c) { idx=check_expr(s,n->c); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"slice bound must be an integer"); } elem=base->elem; n->sem_type=fe_type_slice(&s->c->types,elem); if(base->kind==FE_TYPE_STR) n->flags|=2U; return n->sem_type; } + if(n->c) { + idx=check_expr(s,n->c); + if(known(idx)&&!fe_type_is_integer(idx)) + err(s->c,n->loc,"slice bound must be an integer"); + } + elem=base->elem; + n->sem_type=(base->kind==FE_TYPE_SLICE ? base->ref_mut : + lvalue_writable(s,n->a)) ? + fe_type_mut_slice(&s->c->types,elem) : + fe_type_slice(&s->c->types,elem); + return n->sem_type; + } n->sem_type=base->elem; return n->sem_type; } @@ -715,6 +729,7 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return unknown(c); } n->a->cname = sym->cname; + n->sem_decl = sym->fn; if (!sym->fn) { err(c, n->loc, "name is not a function"); return unknown(c); @@ -725,7 +740,10 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) a = check_expr(s, arg); mark_moved(s,arg,a); b = node_type(c, param->a); - if (!compatible(b, a, arg) && a->kind != FE_TYPE_UNKNOWN) + if (!compatible(b, a, arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind != FE_TYPE_UNKNOWN) err(c, arg->loc, "argument type mismatch"); param = param->next; arg = arg->next; @@ -819,14 +837,15 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) n->sem_type=field->type; return field->type; } if (n && n->kind == FE_N_INDEX) { - if (n->a && n->a->sem_type && n->a->sem_type->kind == FE_TYPE_STR) { - err(s->c, n->loc, "str is immutable"); - } - if (n->a && n->a->kind == FE_N_INDEX && (n->a->flags & 2U)) - err(s->c, n->loc, "str slice is immutable"); - if (!lvalue_writable(s,n->a)) - err(s->c,n->loc,"cannot assign through immutable value"); base=check_index(s,n); + if (n->a && n->a->sem_type && + n->a->sem_type->kind == FE_TYPE_SLICE && + !n->a->sem_type->ref_mut) + err(s->c,n->loc,"cannot write through shared slice"); + else if (n->a && n->a->sem_type && + n->a->sem_type->kind != FE_TYPE_SLICE && + !lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); return base; } if (n) err(s->c, n->loc, "assignment requires a variable"); @@ -903,8 +922,8 @@ static void check_for(FeCheckerState *s, FeNode *n) else if (n->a && n->a->kind==FE_N_INDEX && n->a->a && n->a->a->kind==FE_N_IDENT) iter_sym=find_symbol(s->scope,n->a->a->text ? n->a->a->text : ""); - iter_mut=iter_sym && iter_sym->mutable; - if (start->kind==FE_TYPE_STR) iter_mut=0; + iter_mut=start->kind==FE_TYPE_SLICE ? start->ref_mut : + (iter_sym && iter_sym->mutable); ref_type=fe_type_ref(&s->c->types,elem,iter_mut); if (iter_mut) n->flags |= 4U; s->scope=scope_new(s,old); @@ -1017,6 +1036,8 @@ static void check_stmt(FeCheckerState *s, FeNode *n) err(c, n->loc, "initializer type mismatch"); if (b->kind == FE_TYPE_VOID) err(c, n->loc, "void expression cannot initialize a variable"); + if (n->kind==FE_N_LET && a->kind==FE_TYPE_SLICE && a->ref_mut) + err(c,n->loc,"let cannot bind a mutable slice"); mark_moved(s,n->b,b); add_symbol(s, s->scope, n->text, a, 0, 0, 1, local_cname(c, n->text ? n->text : "local"), n); diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 102a2c5..f445afc 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -75,8 +75,10 @@ static void emit_one_type(FeEmitter *e, FeType *t) } else if(t->kind==FE_TYPE_ARRAY) { fputs(t->cname,e->out); fputs(" { ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" a[",e->out); fprintf(e->out,"%lu",t->length); fputs("]; };\n",e->out); } else if(t->kind==FE_TYPE_SLICE && t->cname) { - fputs("typedef struct { ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); fputs(";\n",e->out); - fprintf(e->out,"static %s %s(%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n",t->cname,t->maker,fe_type_c_name(t->elem,e->pointer_bits),t->cname); + fputs("typedef struct { ",e->out); + if(!t->ref_mut) fputs("const ",e->out); + fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); fputs(";\n",e->out); + fprintf(e->out,"static %s %s(%s%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n",t->cname,t->maker,t->ref_mut ? "" : "const ",fe_type_c_name(t->elem,e->pointer_bits),t->cname); } else if(t->kind==FE_TYPE_ERROR_UNION) { fputs(t->cname,e->out); fputs(" { unsigned short e; ",e->out); fputs(fe_type_c_name(t->error_value,e->pointer_bits),e->out); @@ -97,8 +99,6 @@ static void emit_one_type(FeEmitter *e, FeType *t) static void emit_type_defs(FeEmitter *e) { FeType *t; - fputs("typedef struct { const unsigned char *p; unsigned long n; } fe_str;\n",e->out); - fputs("static fe_str fe_make_str(const unsigned char *p, unsigned long n) { fe_str s; s.p=p; s.n=n; return s; }\n",e->out); /* Every fixed array has a slice conversion helper, even if this unit only indexes the array. Intern those result types before emission so their typedefs are present before helper definitions. */ @@ -246,14 +246,6 @@ static void emit_type_helpers(FeEmitter *e) fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n",t->cname,t->tail_slicer,t->cname,t->slicer); } } - fputs("static unsigned char fe_idx_str(fe_str x, unsigned long i) { ",e->out); - if(!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); - fputs("return x.p[i]; }\n",e->out); - fputs("static fe_str fe_slice_str(fe_str x, unsigned long a, unsigned long b) { ",e->out); - if(!e->no_checks) fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); - fputs("return fe_make_str(x.p+a,b-a); }\n",e->out); - fputs("static fe_str fe_full_slice_str(fe_str x) { return fe_slice_str(x,0,x.n); }\n",e->out); - fputs("static fe_str fe_tail_slice_str(fe_str x, unsigned long a) { return fe_slice_str(x,a,x.n); }\n",e->out); } static void emit_m4_runtime(FeEmitter *e) @@ -276,7 +268,6 @@ static void emit_m4_runtime(FeEmitter *e) fputs("unsigned long fe_m4_sprint_finish(void) { unsigned long result; if (!fe_m4_sprint_depth) abort(); --fe_m4_sprint_depth; result=fe_m4_sprint_stack[fe_m4_sprint_depth].start_n-fe_m4_sprint_stack[fe_m4_sprint_depth].b.n; return result; }\n",e->out); fputs("unsigned short fe_m4_write_bytes(fe_writer w, const unsigned char *p, unsigned long n) { return w.write_fn ? w.write_fn(w.ctx,p,n) : 1; }\n",e->out); fputs("unsigned short fe_m4_write_cstr(fe_writer w, const char *p) { return fe_m4_write_bytes(w,(const unsigned char*)p,(unsigned long)strlen(p)); }\n",e->out); - fputs("unsigned short fe_m4_write_str(fe_writer w, fe_str s) { return fe_m4_write_bytes(w,s.p,s.n); }\n",e->out); fputs("#define fe_m4_write_slice(w,s) fe_m4_write_bytes((w),(s).p,(s).n)\n",e->out); fputs("unsigned short fe_m4_write_int(fe_writer w, long v) { char b[40]; sprintf(b,\"%ld\",v); return fe_m4_write_cstr(w,b); }\n",e->out); fputs("unsigned short fe_m4_write_hex(fe_writer w, unsigned long v) { char b[40]; sprintf(b,\"%lx\",v); return fe_m4_write_cstr(w,b); }\n",e->out); @@ -504,9 +495,8 @@ static void emit_m4_arg(FeEmitter *e, FeNode *arg, int verb, if (verb=='b') { fputs("fe_m4_write_bool(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; } - if (verb=='s' || (verb==' ' && t && (t->kind==FE_TYPE_STR || t->kind==FE_TYPE_SLICE))) { - if (t && t->kind==FE_TYPE_STR) fputs("fe_m4_write_str(",e->out); - else fputs("fe_m4_write_slice(",e->out); + if (verb=='s' || (verb==' ' && t && t->kind==FE_TYPE_SLICE)) { + fputs("fe_m4_write_slice(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; } if (error_value) { @@ -651,7 +641,29 @@ static FeNode *init_field(FeNode *n, const char *name) static void emit_slice_call(FeEmitter *e, FeNode *n) { FeType *bt=n->a ? n->a->sem_type : 0; - const char *maker=bt && bt->slicer ? bt->slicer : "fe_slice_str"; + const char *maker=n->sem_type && n->sem_type->maker ? n->sem_type->maker : + (bt && bt->slicer ? bt->slicer : "fe_missing_slice"); + if (bt && (bt->kind==FE_TYPE_ARRAY || bt->kind==FE_TYPE_SLICE)) { + fputs(n->sem_type && n->sem_type->slicer ? + n->sem_type->slicer : "fe_missing_slicer",e->out); + fputc('(',e->out); fputs(maker,e->out); fputc('(',e->out); + if (bt->kind==FE_TYPE_ARRAY) { + fputs("(&",e->out); emit_lvalue(e,n->a); fputs(")->a",e->out); + } else { + emit_expr(e,n->a); fputs(".p",e->out); + } + fputs(", ",e->out); + if(bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); + else { emit_expr(e,n->a); fputs(".n",e->out); } + fputs("), ",e->out); + if(n->b) emit_expr(e,n->b); else fputs("0",e->out); + fputs(", ",e->out); + if(n->c) emit_expr(e,n->c); + else if(bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); + else { emit_expr(e,n->a); fputs(".n",e->out); } + fputc(')',e->out); + return; + } if (!n->b && !n->c && bt && bt->full_slicer) { fputs(bt->full_slicer,e->out); if (bt->kind==FE_TYPE_ARRAY) { fputs("(&",e->out); emit_lvalue(e,n->a); } @@ -698,7 +710,13 @@ static void emit_expr(FeEmitter *e, FeNode *n) case FE_N_LITERAL: if (n->text && strcmp(n->text, "true") == 0) fputs("1", e->out); else if (n->text && strcmp(n->text, "false") == 0) fputs("0", e->out); - else if (n->text && n->text[0]=='"') { fputs("fe_make_str((const unsigned char*)",e->out); emit_c_literal(e->out,n->text,1); fputs(", sizeof(",e->out); emit_c_literal(e->out,n->text,1); fputs(")-1)",e->out); } + else if (n->text && n->text[0]=='"') { + fputs(n->sem_type && n->sem_type->maker ? + n->sem_type->maker : "fe_missing_str",e->out); + fputs("((const unsigned char*)",e->out); + emit_c_literal(e->out,n->text,1); fputs(", sizeof(",e->out); + emit_c_literal(e->out,n->text,1); fputs(")-1)",e->out); + } else if (n->text && n->text[0]=='\'') emit_c_literal(e->out,n->text,0); else fputs(n->text ? n->text : "0", e->out); break; @@ -775,6 +793,7 @@ static void emit_expr(FeEmitter *e, FeNode *n) break; case FE_N_CALL: { FeVariantType *v; + FeNode *call_param=0; int special=0; if(n->text && (strcmp(n->text,"@print")==0 || strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { emit_m4_builtin(e,n); special=1; } else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && @@ -814,14 +833,25 @@ static void emit_expr(FeEmitter *e, FeNode *n) } else if (n->a) emit_expr(e, n->a); else fputs(n->text ? n->text : "fe_builtin", e->out); if(!special) { + if(n->sem_decl && n->sem_decl->kind==FE_N_FN && n->sem_decl->a) + call_param=n->sem_decl->a->children; fputc('(', e->out); for (x = n->children; x; x = x->next) { + FeType *want=call_param && call_param->a ? + fe_type_from_ast(&e->check->types,call_param->a) : 0; if (x != n->children) fputs(", ", e->out); - if ((x->flags & 0x100U) && x->kind==FE_N_IDENT && + if(want && want->kind==FE_TYPE_SLICE && !want->ref_mut && + x->sem_type && x->sem_type->kind==FE_TYPE_SLICE && + x->sem_type->ref_mut) { + fputs(want->maker,e->out); fputc('(',e->out); + emit_expr(e,x); fputs(".p, ",e->out); + emit_expr(e,x); fputs(".n)",e->out); + } else if ((x->flags & 0x100U) && x->kind==FE_N_IDENT && x->sem_type && x->sem_type->kind==FE_TYPE_OWNED) { fputs("(fe_live_",e->out); fputs(cname(x,"owned"),e->out); fputs("=0, ",e->out); emit_expr(e,x); fputc(')',e->out); } else emit_expr(e, x); + if(call_param) call_param=call_param->next; } fputc(')', e->out); } diff --git a/fec/src/parser.c b/fec/src/parser.c index 9ddd72c..2e9366d 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -49,6 +49,7 @@ static FeNode *type(FeParser *p) if (is(p,FE_TOK_LBRACKET)) { next(p); n=toknode(p,FE_N_TYPE,t); if(!eat(p,FE_TOK_RBRACKET)) { n->a=expr(p,0); want(p,FE_TOK_RBRACKET,"expected ']' in array type"); } + else if(eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"[]mut",5); n->b=type(p); return n; } if (is(p,FE_TOK_FN)) { diff --git a/fec/src/types.c b/fec/src/types.c index f039b6d..c471dcf 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -64,7 +64,8 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) if (strcmp(name, "void") == 0) kind = FE_TYPE_VOID; else if (strcmp(name, "bool") == 0) kind = FE_TYPE_BOOL; else if (strcmp(name, "char") == 0) kind = FE_TYPE_CHAR; - else if (strcmp(name, "str") == 0) kind = FE_TYPE_STR; + else if (strcmp(name, "str") == 0) + return fe_type_slice(ctx, fe_type_intern(ctx, "u8")); else if (strcmp(name, "io.Writer") == 0) { kind = FE_TYPE_STRUCT; } @@ -156,6 +157,26 @@ FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem) return t; } +FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem) +{ + char key[96]; + FeType *t; + sprintf(key, "[]mut %s", elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_SLICE; + t->elem = elem; + t->ref_mut = 1; + t->cname = generated_name(ctx, "fe_mut_slice_", "type"); + t->maker = generated_name(ctx, "fe_make_mut_slice_", "type"); + t->indexer = generated_name(ctx, "fe_idx_mut_slice_", "type"); + t->slicer = generated_name(ctx, "fe_slice_mut_slice_", "type"); + t->full_slicer = generated_name(ctx, "fe_full_mut_slice_", "type"); + t->tail_slicer = generated_name(ctx, "fe_tail_mut_slice_", "type"); + } + return t; +} + FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) { char key[128]; @@ -478,7 +499,7 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) if (node->text && strcmp(node->text, "as") == 0) return fe_type_from_ast(ctx, node->b); if (node->text && strcmp(node->text, "str") == 0) - return fe_type_intern(ctx, "str"); + return fe_type_slice(ctx, fe_type_intern(ctx, "u8")); if (node->a && node->a->kind==FE_N_IDENT && node->text && strcmp(node->text,"io")==0 && node->a->text) { sprintf(qualified,"%s.%s",node->text,node->a->text); @@ -490,13 +511,16 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) strcmp(node->text,"&mut") == 0); if (node->text && strcmp(node->text,"^")==0) return fe_type_owned(ctx,fe_type_from_ast(ctx,node->a)); - if (node->text && strcmp(node->text, "[") == 0) { + if (node->text && (strcmp(node->text, "[") == 0 || + strcmp(node->text, "[]mut") == 0)) { if (node->a) { if (node->a->kind == FE_N_LITERAL && node->a->text) length = strtoul(node->a->text, 0, 0); return fe_type_array(ctx, length, fe_type_from_ast(ctx, node->b)); } - return fe_type_slice(ctx, fe_type_from_ast(ctx, node->b)); + return strcmp(node->text,"[]mut")==0 ? + fe_type_mut_slice(ctx, fe_type_from_ast(ctx,node->b)) : + fe_type_slice(ctx, fe_type_from_ast(ctx, node->b)); } if (node->text && strcmp(node->text, "!") == 0) /* Prefix !T stores T in a; the E!T spelling stores its success diff --git a/fec/src/types.h b/fec/src/types.h index 4acab3c..fae31ae 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -73,6 +73,7 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name); FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node); FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem); FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem); +FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable); FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value); diff --git a/fec/test-dos.bat b/fec/test-dos.bat index 70026fb..be85d12 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -15,9 +15,9 @@ fec.exe --dump-ast TESTS\PASS\BASIC.FE > nul if errorlevel 1 goto test_fail fec.exe --dump-ast TESTS\PASS\LITERALS.FE > nul if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\PASS\KEYWORDS-AND-BUILTINS.FE > nul +fec.exe --dump-ast TESTS\PASS\KEYBUILT.FE > nul if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\PASS\V012-FORMS.FE > nul +fec.exe --dump-ast TESTS\PASS\V012FORM.FE > nul if errorlevel 1 goto test_fail fec.exe --dump-ast STD\CORE.FE > nul @@ -37,11 +37,11 @@ if errorlevel 1 goto test_fail fec.exe --dump-ast STD\SYS.FE > nul if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\MISSING-SEMI.FE > nul +fec.exe --dump-ast TESTS\FAIL\MISSSEMI.FE > nul if not errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\UNCLOSED-COMMENT.FE > nul +fec.exe --dump-ast TESTS\FAIL\UNCLCOMM.FE > nul if not errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\LOGICAL-SYMBOLS.FE > nul +fec.exe --dump-ast TESTS\FAIL\LOGICAL.FE > nul if not errorlevel 1 goto test_fail if exist TESTS\M2\HELLO.C del TESTS\M2\HELLO.C @@ -67,28 +67,28 @@ TESTS\M2\SCOPES.EXE if errorlevel 1 goto test_fail rem M2 bits16 regression path remains on compiler A (wcl). -fec.exe --target=bits16 --emit-c TESTS\M2\CAST-WHILE.FE -o TESTS\M2\CAST16.C > nul +fec.exe --target=bits16 --emit-c TESTS\M2\CASTWHIL.FE -o TESTS\M2\CAST16.C > nul if errorlevel 1 goto test_fail wcl -q -za -bt=dos -fe=TESTS\M2\CAST16.EXE TESTS\M2\CAST16.C if errorlevel 1 goto test_fail TESTS\M2\CAST16.EXE if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CONDITION.FE -o TESTS\M2\BAD-CO.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-COND.FE -o TESTS\M2\BAD-CO.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CAST.FE -o TESTS\M2\BAD-CA.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ASSIGN.FE -o TESTS\M2\BAD-AS.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ASGN.FE -o TESTS\M2\BAD-AS.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNKNOWN.FE -o TESTS\M2\BAD-UN.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNK.FE -o TESTS\M2\BAD-UN.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ARITY.FE -o TESTS\M2\BAD-AR.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ARI.FE -o TESTS\M2\BAD-AR.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-TYPES.FE -o TESTS\M2\BAD-TY.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-TYPE.FE -o TESTS\M2\BAD-TY.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-RETURN.FE -o TESTS\M2\BAD-RE.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-RET.FE -o TESTS\M2\BAD-RE.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNINIT.FE -o TESTS\M2\BAD-UI.C > nul +fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNIT.FE -o TESTS\M2\BAD-UI.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M2\BAD-VOID.FE -o TESTS\M2\BAD-VO.C > nul if not errorlevel 1 goto test_fail @@ -132,6 +132,16 @@ wcl386 -q -za -bt=dos -fe=TESTS\M3\ARRAY.EXE TESTS\M3\ARRAY.C if errorlevel 1 goto test_fail TESTS\M3\ARRAY.EXE if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\MUTABLE.FE -o TESTS\M3\MUTABLE.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\MUTABLE.EXE TESTS\M3\MUTABLE.C +if errorlevel 1 goto test_fail +TESTS\M3\MUTABLE.EXE +if errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BAD-MLET.FE -o TESTS\M3\BAD-MLET.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\BAD-SHWR.FE -o TESTS\M3\BAD-SHWR.C > nul +if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M3\STR.FE -o TESTS\M3\STR.C > nul if errorlevel 1 goto test_fail wcl386 -q -za -bt=dos -fe=TESTS\M3\STR.EXE TESTS\M3\STR.C @@ -168,6 +178,12 @@ wcl386 -q -za -bt=dos -fe=TESTS\M3\BOUNDS.EXE TESTS\M3\BOUNDS.C if errorlevel 1 goto test_fail TESTS\M3\BOUNDS.EXE if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M3\SLCBOUND.FE -o TESTS\M3\SLCBOUND.C > nul +if errorlevel 1 goto test_fail +wcl386 -q -za -bt=dos -fe=TESTS\M3\SLCBOUND.EXE TESTS\M3\SLCBOUND.C +if errorlevel 1 goto test_fail +TESTS\M3\SLCBOUND.EXE +if not errorlevel 1 goto test_fail fec.exe --target=bits32 --no-checks --emit-c TESTS\M3\BOUNDS.FE -o TESTS\M3\BOUNDS-N.C > nul if errorlevel 1 goto test_fail wcl386 -q -za -bt=dos -fe=TESTS\M3\BOUNDS-N.EXE TESTS\M3\BOUNDS-N.C @@ -197,7 +213,7 @@ wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\FORMAT.EXE TESTS\M4\FORMAT.C if errorlevel 1 goto test_fail TESTS\M4\FORMAT.EXE > nul if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\TRY-FPRINT.FE -o TESTS\M4\TRY-FPR.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\TRY-FPR.FE -o TESTS\M4\TRY-FPR.C > nul if errorlevel 1 goto test_fail wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\TRY-FPR.EXE TESTS\M4\TRY-FPR.C if errorlevel 1 goto test_fail @@ -209,17 +225,17 @@ wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\PROP.EXE TESTS\M4\PROPTEST.C if errorlevel 1 goto test_fail TESTS\M4\PROP.EXE > nul if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-ARITY.FE -o TESTS\M4\BAD-ARI.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-ARI.FE -o TESTS\M4\BAD-ARI.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-VERB.FE -o TESTS\M4\BAD-VERB.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-RUNTIME.FE -o TESTS\M4\BAD-RUN.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-RUN.FE -o TESTS\M4\BAD-RUN.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TYPE.FE -o TESTS\M4\BAD-TYP.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TRY.FE -o TESTS\M4\BAD-TRY.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-WRITER.FE -o TESTS\M4\BAD-WRI.C > nul +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-WRIT.FE -o TESTS\M4\BAD-WRI.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-MANY.FE -o TESTS\M4\BAD-MANY.C > nul if not errorlevel 1 goto test_fail @@ -233,13 +249,13 @@ fec.exe --target=bits32 --emit-c TESTS\M5\OWNED.FE -o TESTS\M5\OWNED.C > nul if errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M5\BAD-MOVE.FE -o TESTS\M5\BAD-MOVE.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DESTROY.FE -o TESTS\M5\BAD-DES.C > nul +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DEST.FE -o TESTS\M5\BAD-DES.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DROP.FE -o TESTS\M5\BAD-DROP.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DOUBLE.FE -o TESTS\M5\BAD-DBL.C > nul +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DBL.FE -o TESTS\M5\BAD-DBL.C > nul if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-CONDITIONAL.FE -o TESTS\M5\BAD-COND.C > nul +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-COND.FE -o TESTS\M5\BAD-COND.C > nul if not errorlevel 1 goto test_fail if exist TESTS\M5\RUNTIME-G.C del TESTS\M5\RUNTIME-G.C if exist TESTS\M5\RUNTIME.O del TESTS\M5\RUNTIME.O diff --git a/fec/tests/fail/logical-symbols.fe b/fec/tests/fail/logical.fe similarity index 100% rename from fec/tests/fail/logical-symbols.fe rename to fec/tests/fail/logical.fe diff --git a/fec/tests/fail/missing-semi.fe b/fec/tests/fail/misssemi.fe similarity index 100% rename from fec/tests/fail/missing-semi.fe rename to fec/tests/fail/misssemi.fe diff --git a/fec/tests/fail/unclosed-comment.fe b/fec/tests/fail/unclcomm.fe similarity index 100% rename from fec/tests/fail/unclosed-comment.fe rename to fec/tests/fail/unclcomm.fe diff --git a/fec/tests/m2/bad-arity.fe b/fec/tests/m2/bad-ari.fe similarity index 100% rename from fec/tests/m2/bad-arity.fe rename to fec/tests/m2/bad-ari.fe diff --git a/fec/tests/m2/bad-assign.fe b/fec/tests/m2/bad-asgn.fe similarity index 100% rename from fec/tests/m2/bad-assign.fe rename to fec/tests/m2/bad-asgn.fe diff --git a/fec/tests/m2/bad-condition.fe b/fec/tests/m2/bad-cond.fe similarity index 100% rename from fec/tests/m2/bad-condition.fe rename to fec/tests/m2/bad-cond.fe diff --git a/fec/tests/m2/bad-return.fe b/fec/tests/m2/bad-ret.fe similarity index 100% rename from fec/tests/m2/bad-return.fe rename to fec/tests/m2/bad-ret.fe diff --git a/fec/tests/m2/bad-types.fe b/fec/tests/m2/bad-type.fe similarity index 100% rename from fec/tests/m2/bad-types.fe rename to fec/tests/m2/bad-type.fe diff --git a/fec/tests/m2/bad-uninit.fe b/fec/tests/m2/bad-unit.fe similarity index 100% rename from fec/tests/m2/bad-uninit.fe rename to fec/tests/m2/bad-unit.fe diff --git a/fec/tests/m2/bad-unknown.fe b/fec/tests/m2/bad-unk.fe similarity index 100% rename from fec/tests/m2/bad-unknown.fe rename to fec/tests/m2/bad-unk.fe diff --git a/fec/tests/m2/cast-while.fe b/fec/tests/m2/castwhil.fe similarity index 100% rename from fec/tests/m2/cast-while.fe rename to fec/tests/m2/castwhil.fe diff --git a/fec/tests/m3/bad-mlet.fe b/fec/tests/m3/bad-mlet.fe new file mode 100644 index 0000000..af257c1 --- /dev/null +++ b/fec/tests/m3/bad-mlet.fe @@ -0,0 +1,6 @@ +unit m3_bad_mut_let; + +fn bad() -> void { + var raw: [2]u8 = [1, 2]; + let s: []mut u8 = raw[..]; +} diff --git a/fec/tests/m3/bad-shwr.fe b/fec/tests/m3/bad-shwr.fe new file mode 100644 index 0000000..aa4f105 --- /dev/null +++ b/fec/tests/m3/bad-shwr.fe @@ -0,0 +1,5 @@ +unit m3_bad_shared_write; + +fn bad(s: []u8) -> void { + s[0] = 1; +} diff --git a/fec/tests/m3/mutable.fe b/fec/tests/m3/mutable.fe new file mode 100644 index 0000000..9c31d51 --- /dev/null +++ b/fec/tests/m3/mutable.fe @@ -0,0 +1,11 @@ +unit m3_mutable; + +fn takes_shared(s: []u8) -> u8 { return s[0]; } + +fn main() -> i32 { + var raw: [3]u8 = [1, 2, 3]; + var s: []mut u8 = raw[..]; + s[1] = 9; + if takes_shared(s) == 1 and raw[1] == 9 { return 0; } + return 1; +} diff --git a/fec/tests/m3/slcbound.fe b/fec/tests/m3/slcbound.fe new file mode 100644 index 0000000..89a8d88 --- /dev/null +++ b/fec/tests/m3/slcbound.fe @@ -0,0 +1,7 @@ +unit m3_slice_bounds; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let s: []i32 = a[0..3]; + return s.n as i32; +} diff --git a/fec/tests/m4/bad-arity.fe b/fec/tests/m4/bad-ari.fe similarity index 100% rename from fec/tests/m4/bad-arity.fe rename to fec/tests/m4/bad-ari.fe diff --git a/fec/tests/m4/bad-runtime.fe b/fec/tests/m4/bad-run.fe similarity index 100% rename from fec/tests/m4/bad-runtime.fe rename to fec/tests/m4/bad-run.fe diff --git a/fec/tests/m4/bad-writer.fe b/fec/tests/m4/bad-writ.fe similarity index 100% rename from fec/tests/m4/bad-writer.fe rename to fec/tests/m4/bad-writ.fe diff --git a/fec/tests/m4/try-fprint.fe b/fec/tests/m4/try-fpr.fe similarity index 100% rename from fec/tests/m4/try-fprint.fe rename to fec/tests/m4/try-fpr.fe diff --git a/fec/tests/m5/bad-conditional.fe b/fec/tests/m5/bad-cond.fe similarity index 100% rename from fec/tests/m5/bad-conditional.fe rename to fec/tests/m5/bad-cond.fe diff --git a/fec/tests/m5/bad-double.fe b/fec/tests/m5/bad-dbl.fe similarity index 100% rename from fec/tests/m5/bad-double.fe rename to fec/tests/m5/bad-dbl.fe diff --git a/fec/tests/m5/bad-destroy.fe b/fec/tests/m5/bad-dest.fe similarity index 100% rename from fec/tests/m5/bad-destroy.fe rename to fec/tests/m5/bad-dest.fe diff --git a/fec/tests/pass/keywords-and-builtins.fe b/fec/tests/pass/keybuilt.fe similarity index 100% rename from fec/tests/pass/keywords-and-builtins.fe rename to fec/tests/pass/keybuilt.fe diff --git a/fec/tests/pass/v012-forms.fe b/fec/tests/pass/v012form.fe similarity index 100% rename from fec/tests/pass/v012-forms.fe rename to fec/tests/pass/v012form.fe From 351e5dbb231eb96f7e8899f14bc1649d1281f997 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 18:27:26 +0900 Subject: [PATCH 036/184] feat: replace M4 callback writers with safe handles --- fec/src/check.c | 21 ++++++--------------- fec/src/emit_c.c | 33 ++++++++++++--------------------- fec/std/fmt.fe | 8 +++++--- fec/std/io.fe | 11 ++++++----- fec/test-dos.bat | 2 ++ fec/tests/m4/bad-bufw.fe | 7 +++++++ fec/tests/m4/bad-writ.fe | 2 +- fec/tests/m4/format.fe | 14 +++++++------- fec/tests/m4/prop.fe | 2 +- fec/tests/m4/proptest.c | 20 ++++---------------- fec/tests/m4/try-fpr.fe | 6 +++--- 11 files changed, 54 insertions(+), 72 deletions(-) create mode 100644 fec/tests/m4/bad-bufw.fe diff --git a/fec/src/check.c b/fec/src/check.c index 4bda576..711159d 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -375,9 +375,8 @@ static void check_format_call(FeCheckerState *s, FeNode *n) if (offset) { if (!fmt_node) { err(s->c,n->loc,"@fprint requires a writer"); return; } t=check_expr(s,fmt_node); - if (!(t && t->kind==FE_TYPE_REF && t->ref_mut && - format_is_writer_type(t->elem))) - err(s->c,fmt_node->loc,"@fprint requires &mut io.Writer"); + if (!format_is_writer_type(t)) + err(s->c,fmt_node->loc,"@fprint requires io.Writer"); fmt_node=fmt_node->next; } if (strcmp(n->text,"@sprint")==0) { @@ -679,18 +678,9 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && - (strcmp(n->a->b->text,"buf_writer")==0 || - strcmp(n->a->b->text,"null_writer")==0)) { + strcmp(n->a->b->text,"null_writer")==0) { FeNode *arg=n->children; - if (strcmp(n->a->b->text,"buf_writer")==0) { - if (!arg) err(c,n->loc,"io.buf_writer requires a buffer"); - else { - a=check_expr(s,arg); - if (!(a && a->kind==FE_TYPE_REF && a->ref_mut && - format_is_slice_u8(a->elem))) - err(c,arg->loc,"io.buf_writer requires &mut []u8 buffer"); - } - } else if (arg) err(c,n->loc,"io.null_writer takes no arguments"); + if (arg) err(c,n->loc,"io.null_writer takes no arguments"); n->sem_type=fe_type_intern(&c->types,"io.Writer"); return n->sem_type; } @@ -760,7 +750,8 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) if (n->kind == FE_N_MEMBER) { if (n->a && n->a->kind==FE_N_IDENT && n->a->text && strcmp(n->a->text,"io")==0 && n->b && n->b->text && - strcmp(n->b->text,"stdout")==0) { + (strcmp(n->b->text,"stdout")==0 || + strcmp(n->b->text,"stderr")==0)) { n->sem_type=fe_type_intern(&c->types,"io.Writer"); return n->sem_type; } diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index f445afc..0df0575 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -251,22 +251,20 @@ static void emit_type_helpers(FeEmitter *e) static void emit_m4_runtime(FeEmitter *e) { fputs("typedef struct { unsigned char *p; unsigned long n; } fe_m4_slice;\n",e->out); - fputs("typedef struct { void *ctx; unsigned short (*write_fn)(void *, const unsigned char *, unsigned long); } fe_writer;\n",e->out); + fputs("typedef struct { unsigned char tag; unsigned short handle; } fe_writer;\n",e->out); fputs("unsigned short fe_m4_error;\n",e->out); - fputs("unsigned short fe_m4_stdout_write(void *ctx, const unsigned char *p, unsigned long n) { (void)ctx; return fwrite(p,1,(size_t)n,stdout)==(size_t)n ? 0 : 1; }\n",e->out); - fputs("unsigned short fe_m4_null_write(void *ctx, const unsigned char *p, unsigned long n) { (void)ctx; (void)p; (void)n; return 0; }\n",e->out); - fputs("unsigned short fe_m4_buf_write(void *ctx, const unsigned char *p, unsigned long n) { fe_m4_slice *b=(fe_m4_slice*)ctx; unsigned long k=nn?n:b->n; if(k) memcpy(b->p,p,(size_t)k); b->p+=k; b->n-=k; return 0; }\n",e->out); - fputs("fe_writer fe_m4_stdout_writer(void) { fe_writer w; w.ctx=0; w.write_fn=fe_m4_stdout_write; return w; }\n",e->out); - fputs("fe_writer fe_m4_null_writer(void) { fe_writer w; w.ctx=0; w.write_fn=fe_m4_null_write; return w; }\n",e->out); - fputs("fe_writer fe_m4_buf_writer(fe_m4_slice *b) { fe_writer w; w.ctx=b; w.write_fn=fe_m4_buf_write; return w; }\n",e->out); + fputs("fe_writer fe_m4_writer(unsigned char tag, unsigned short handle) { fe_writer w; w.tag=tag; w.handle=handle; return w; }\n",e->out); + fputs("fe_writer fe_m4_stdout_writer(void) { return fe_m4_writer(0,1); }\n",e->out); + fputs("fe_writer fe_m4_stderr_writer(void) { return fe_m4_writer(1,2); }\n",e->out); + fputs("fe_writer fe_m4_null_writer(void) { return fe_m4_writer(3,0); }\n",e->out); fputs("/* bounded sprint stack; overflow traps instead of corrupting an outer call */\n#define FE_M4_SPRINT_DEPTH 8\n",e->out); fputs("typedef struct { fe_m4_slice b; unsigned long start_n; } fe_m4_sprint_frame;\n",e->out); fputs("static fe_m4_sprint_frame fe_m4_sprint_stack[FE_M4_SPRINT_DEPTH];\n",e->out); fputs("static unsigned fe_m4_sprint_depth;\n",e->out); fputs("void fe_m4_sprint_begin(fe_m4_slice *b) { if (fe_m4_sprint_depth>=FE_M4_SPRINT_DEPTH) abort(); fe_m4_sprint_stack[fe_m4_sprint_depth].b=*b; fe_m4_sprint_stack[fe_m4_sprint_depth].start_n=b->n; ++fe_m4_sprint_depth; }\n",e->out); - fputs("fe_writer fe_m4_sprint_writer(void) { return fe_m4_buf_writer(&fe_m4_sprint_stack[fe_m4_sprint_depth-1].b); }\n",e->out); + fputs("fe_writer fe_m4_sprint_writer(void) { return fe_m4_writer(4,(unsigned short)(fe_m4_sprint_depth-1)); }\n",e->out); fputs("unsigned long fe_m4_sprint_finish(void) { unsigned long result; if (!fe_m4_sprint_depth) abort(); --fe_m4_sprint_depth; result=fe_m4_sprint_stack[fe_m4_sprint_depth].start_n-fe_m4_sprint_stack[fe_m4_sprint_depth].b.n; return result; }\n",e->out); - fputs("unsigned short fe_m4_write_bytes(fe_writer w, const unsigned char *p, unsigned long n) { return w.write_fn ? w.write_fn(w.ctx,p,n) : 1; }\n",e->out); + fputs("unsigned short fe_m4_write_bytes(fe_writer w, const unsigned char *p, unsigned long n) { if(w.tag==0) return fwrite(p,1,(size_t)n,stdout)==(size_t)n?0:1; if(w.tag==1) return fwrite(p,1,(size_t)n,stderr)==(size_t)n?0:1; if(w.tag==3) return 0; if(w.tag==4 && w.handlen?n:b->n; if(k) memcpy(b->p,p,(size_t)k); b->p+=k; b->n-=k; return 0; } return 1; }\n",e->out); fputs("unsigned short fe_m4_write_cstr(fe_writer w, const char *p) { return fe_m4_write_bytes(w,(const unsigned char*)p,(unsigned long)strlen(p)); }\n",e->out); fputs("#define fe_m4_write_slice(w,s) fe_m4_write_bytes((w),(s).p,(s).n)\n",e->out); fputs("unsigned short fe_m4_write_int(fe_writer w, long v) { char b[40]; sprintf(b,\"%ld\",v); return fe_m4_write_cstr(w,b); }\n",e->out); @@ -456,13 +454,8 @@ static void emit_m4_piece(FILE *out, const char *fmt, unsigned long begin, static void emit_m4_writer(FeEmitter *e, FeNode *arg, int buffer) { - if (buffer) { - fputs("fe_m4_buf_writer((fe_m4_slice*)&",e->out); - if (arg && arg->kind==FE_N_UNARY && arg->text && - (strcmp(arg->text,"&")==0 || strcmp(arg->text,"&mut")==0)) emit_expr(e,arg->a); - else emit_expr(e,arg); - fputs(")",e->out); - } else if (arg && arg->kind==FE_N_UNARY && arg->text && + (void)buffer; + if (arg && arg->kind==FE_N_UNARY && arg->text && (strcmp(arg->text,"&")==0 || strcmp(arg->text,"&mut")==0)) { emit_expr(e,arg->a); } else if (arg && arg->kind==FE_N_CALL && arg->a && @@ -817,10 +810,6 @@ static void emit_expr(FeEmitter *e, FeNode *n) fputs("()",e->out); special=1; } - else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"buf_writer")==0 && n->children) { emit_m4_writer(e,n->children,1); special=1; } else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && @@ -861,7 +850,9 @@ static void emit_expr(FeEmitter *e, FeNode *n) FeVariantType *v; if(n->a && n->a->kind==FE_N_IDENT && n->a->text && strcmp(n->a->text,"io")==0 && n->b && n->b->text && - strcmp(n->b->text,"stdout")==0) fputs("fe_m4_stdout_writer()",e->out); + (strcmp(n->b->text,"stdout")==0 || strcmp(n->b->text,"stderr")==0)) + fputs(strcmp(n->b->text,"stderr")==0 ? + "fe_m4_stderr_writer()" : "fe_m4_stdout_writer()",e->out); else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && n->b && n->b->text && strcmp(n->b->text,"^")==0) { fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); diff --git a/fec/std/fmt.fe b/fec/std/fmt.fe index 460cc0b..e63bc0b 100644 --- a/fec/std/fmt.fe +++ b/fec/std/fmt.fe @@ -1,4 +1,6 @@ unit fmt; -pub fn write_str(w: &mut io.Writer, s: str) -> !void; -pub fn write_int_i32(w: &mut io.Writer, v: i32) -> !void; -pub fn write_bool(w: &mut io.Writer, v: bool) -> !void; +pub fn fmt_int_i32(buf: []mut u8, v: i32) -> str; +pub fn fmt_hex_i32(buf: []mut u8, v: i32) -> str; +pub fn fmt_char(buf: []mut u8, v: char) -> str; +pub fn fmt_bool(buf: []mut u8, v: bool) -> str; +pub fn fmt_error(buf: []mut u8, v: core.Error) -> str; diff --git a/fec/std/io.fe b/fec/std/io.fe index 2f4f983..d7cc685 100644 --- a/fec/std/io.fe +++ b/fec/std/io.fe @@ -1,9 +1,10 @@ unit io; -pub struct Writer { - ctx: *void, - write_fn: fn(*void, []u8) -> !usize, -} +pub enum Writer { Stdout, Stderr, File(u16), Null } +pub enum Reader { Stdin, File(u16) } +pub fn write(w: Writer, bytes: []u8) -> !usize; +pub fn read(r: Reader, bytes: []mut u8) -> !usize; pub struct File { handle: u16, - pub fn close(self: &mut Self) { } + pub fn close(self: Self) -> !void; + pub fn drop(self: &mut Self) { } } diff --git a/fec/test-dos.bat b/fec/test-dos.bat index be85d12..60acb3e 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -237,6 +237,8 @@ fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TRY.FE -o TESTS\M4\BAD-TRY.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-WRIT.FE -o TESTS\M4\BAD-WRI.C > nul if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M4\BAD-BUFW.FE -o TESTS\M4\BAD-BUFW.C > nul +if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-MANY.FE -o TESTS\M4\BAD-MANY.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M4\BAD-OPEN.FE -o TESTS\M4\BAD-OPEN.C > nul diff --git a/fec/tests/m4/bad-bufw.fe b/fec/tests/m4/bad-bufw.fe new file mode 100644 index 0000000..791bd7d --- /dev/null +++ b/fec/tests/m4/bad-bufw.fe @@ -0,0 +1,7 @@ +unit m4_bad_buffer_writer; + +fn main() -> void { + var raw: [4]u8 = [0, 0, 0, 0]; + var buf: []mut u8 = raw[..]; + let w = io.buf_writer(buf); +} diff --git a/fec/tests/m4/bad-writ.fe b/fec/tests/m4/bad-writ.fe index 8dac8c4..794ed64 100644 --- a/fec/tests/m4/bad-writ.fe +++ b/fec/tests/m4/bad-writ.fe @@ -2,6 +2,6 @@ unit m4_bad_writer; fn main() -> i32 { var x: i32 = 0; - @fprint(&mut x, "bad"); + @fprint(x, "bad"); return 0; } diff --git a/fec/tests/m4/format.fe b/fec/tests/m4/format.fe index e3582fd..2445625 100644 --- a/fec/tests/m4/format.fe +++ b/fec/tests/m4/format.fe @@ -8,15 +8,15 @@ fn main() -> i32 { var raw3: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; var raw4: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; var raw5: [8]u8 = [0, 0, 0, 0, 0, 0, 0, 0]; - var buf: []u8 = raw[..]; - var buf2: []u8 = raw2[..]; - var buf3: []u8 = raw3[..]; - var buf4: []u8 = raw4[..]; - var buf5: []u8 = raw5[..]; - let w: io.Writer = io.buf_writer(&mut buf); + var buf: []mut u8 = raw[..]; + var buf2: []mut u8 = raw2[..]; + var buf3: []mut u8 = raw3[..]; + var buf4: []mut u8 = raw4[..]; + var buf5: []mut u8 = raw5[..]; + let w: io.Writer = io.null_writer(); const LOCAL_FMT: str = "value={}\n"; @print(FMT, 7, 15, 'A', "yes", true); - @fprint(&mut w, LOCAL_FMT, 12); + @fprint(w, LOCAL_FMT, 12); let n: usize = @sprint(buf2, "A\x42\u0043defghi"); let n2: usize = @sprint(buf3, "xy"); let inner_n: usize = @sprint(buf4, "xy"); diff --git a/fec/tests/m4/prop.fe b/fec/tests/m4/prop.fe index 81a85f4..878a77a 100644 --- a/fec/tests/m4/prop.fe +++ b/fec/tests/m4/prop.fe @@ -1,5 +1,5 @@ unit m4_prop; -pub fn propagate(w: &mut io.Writer) -> !void { +pub fn propagate(w: io.Writer) -> !void { try @fprint(w, "a{}b", 1); } diff --git a/fec/tests/m4/proptest.c b/fec/tests/m4/proptest.c index 7261461..66bba1a 100644 --- a/fec/tests/m4/proptest.c +++ b/fec/tests/m4/proptest.c @@ -1,23 +1,11 @@ #include "prop.c" -static unsigned short calls; - -static unsigned short fail_write(void *ctx, const unsigned char *p, - unsigned long n) -{ - (void)ctx; - (void)p; - (void)n; - ++calls; - return calls == 1 ? 7 : 0; -} - int main(void) { fe_writer w; unsigned short result; - w.ctx=0; - w.write_fn=fail_write; - result=fe_m4_prop_propagate(&w); - return (result==7 && calls==1) ? 0 : 1; + w.tag=2; + w.handle=99; + result=fe_m4_prop_propagate(w); + return result==1 ? 0 : 1; } diff --git a/fec/tests/m4/try-fpr.fe b/fec/tests/m4/try-fpr.fe index e6c419c..94736cc 100644 --- a/fec/tests/m4/try-fpr.fe +++ b/fec/tests/m4/try-fpr.fe @@ -2,7 +2,7 @@ unit m4_try_fprint; fn main() -> !void { var raw: [4]u8 = [0, 0, 0, 0]; - var buf: []u8 = raw[..]; - let w: io.Writer = io.buf_writer(&mut buf); - try @fprint(&mut w, "ok"); + var buf: []mut u8 = raw[..]; + let w: io.Writer = io.null_writer(); + try @fprint(w, "ok"); } From deafd27939221c8387c5b19cfcb2c7c74318c5e1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 18:48:35 +0900 Subject: [PATCH 037/184] feat: complete M5 ownership and cleanup --- AGENTS.md | 10 +- fec/src/check.c | 209 +++++++++++++++++++++++++++++++++++---- fec/src/emit_c.c | 191 ++++++++++++++++++++++++++++------- fec/src/types.c | 15 ++- fec/src/types.h | 2 + fec/std/mem.fe | 6 +- fec/test-dos.bat | 12 ++- fec/tests/m5/bad-clos.fe | 13 +++ fec/tests/m5/bad-loop.fe | 7 ++ fec/tests/m5/bad-proj.fe | 8 ++ fec/tests/m5/owned.fe | 4 +- fec/tests/m5/runtime.c | 64 ++++++++++-- fec/tests/m5/runtime.fe | 85 ++++++++++++++-- 13 files changed, 545 insertions(+), 81 deletions(-) create mode 100644 fec/tests/m5/bad-clos.fe create mode 100644 fec/tests/m5/bad-loop.fe create mode 100644 fec/tests/m5/bad-proj.fe diff --git a/AGENTS.md b/AGENTS.md index d48bfeb..629b919 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -63,12 +63,8 @@ VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 ## 현재 상태 -- M1~M4 완료 및 QEMU/Open Watcom 검증됨. -- M5(`^T`, drop, defer, 이동 검사) 진행 중. 남은 것: 모든 경로에서 정확히 1회 - cleanup, defer와 drop의 선언 역순 병합, `try` 전파 경로 cleanup, `MaybeMoved` - 런타임 live flag, struct drop과 필드 역순 drop, 분기/루프 상태 합류, - 누수·이중해제 카운터 harness. -- `own.c/h`가 아직 없고 소유권 로직이 `check.c`/`emit_c.c`에 들어가 있다. R1~R8 - 전체를 다루는 **M6 착수 시점에 분리한다** (`SPEC.md` §11.3). +- M1~M5 완료 및 QEMU/Open Watcom 검증됨. +- 다음은 M6(R1~R8 대여 검사)다. 착수할 때 소유권 로직을 `check.c`/`emit_c.c`에서 + `own.c/h`로 분리한다 (`SPEC.md` §11.3). - v0.1.6에서 R8(파생 반환), R6(마지막 사용까지 대여), R10(전역 대여 금지)이 바뀌었다. 셋 다 own.c의 상태 기계를 건드리므로 분리 이후에 함께 구현한다. diff --git a/fec/src/check.c b/fec/src/check.c index 711159d..ad5abb3 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -72,7 +72,15 @@ static int is_copy_type(FeType *t) static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) { FeSym *sym; - if (!n || !t || is_copy_type(t) || n->kind!=FE_N_IDENT) return; + if (!n || !t || is_copy_type(t)) return; + if(n->kind==FE_N_INDEX && t->kind==FE_TYPE_SLICE && + (n->c || !n->b)) return; + if(n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX) { + err(s->c,n->loc, + "cannot move a non-Copy value out of a projection; use mem.replace"); + return; + } + if(n->kind!=FE_N_IDENT) return; sym=find_symbol(s->scope,n->text ? n->text : ""); if (sym) { if (s->defer_depth) { @@ -258,6 +266,31 @@ void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, } static FeType *check_expr(FeCheckerState *s, FeNode *n); + +static FeNode *find_method(FeCheck *c, FeType *owner, const char *name) +{ + FeNode *decl; + FeNode *method; + if(!owner || !name) return 0; + for(decl=c->ast->root ? c->ast->root->children : 0; decl; decl=decl->next) + if(decl->kind==FE_N_STRUCT && decl->text && + strcmp(decl->text,owner->name)==0) + for(method=decl->children; method; method=method->next) + if(method->kind==FE_N_FN && method->text && + strcmp(method->text,name)==0) return method; + return 0; +} + +static FeType *method_type(FeCheck *c, FeNode *node, FeType *owner) +{ + if(node && node->kind==FE_N_TYPE && node->text && + strcmp(node->text,"Self")==0) return owner; + if(node && node->kind==FE_N_TYPE && node->text && + (strcmp(node->text,"&")==0 || strcmp(node->text,"&mut")==0) && + node->a && node->a->text && strcmp(node->a->text,"Self")==0) + return fe_type_ref(&c->types,owner,strcmp(node->text,"&mut")==0); + return node_type(c,node); +} static void check_match(FeCheckerState *s, FeNode *n); static void check_stmt(FeCheckerState *s, FeNode *n); @@ -666,14 +699,44 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return n->sem_type; } if (strcmp(n->a->b->text,"create")==0) { - if (!arg || arg->next || arg->kind!=FE_N_IDENT) - err(c,n->loc,"mem.create requires exactly one type argument"); - a=arg && arg->kind==FE_N_IDENT ? - fe_type_owned(&c->types,fe_type_intern(&c->types,arg->text)) : - fe_type_owned(&c->types,unknown(c)); + if (!arg || arg->next) + err(c,n->loc,"mem.create requires exactly one value"); + a=arg ? check_expr(s,arg) : unknown(c); + if(arg) mark_moved(s,arg,a); + a=fe_type_owned(&c->types,a); n->sem_type=fe_type_error_union(&c->types,a); return n->sem_type; } + if (strcmp(n->a->b->text,"alloc_slice")==0) { + FeNode *count=arg ? arg->next : 0; + FeType *item; + if(!arg || arg->kind!=FE_N_IDENT || !count || count->next) + err(c,n->loc,"mem.alloc_slice requires a type and length"); + item=arg && arg->kind==FE_N_IDENT ? + fe_type_intern(&c->types,arg->text) : unknown(c); + b=count ? check_expr(s,count) : unknown(c); + if(known(b) && !fe_type_is_integer(b)) + err(c,count->loc,"slice length must be an integer"); + a=fe_type_owned(&c->types,fe_type_slice(&c->types,item)); + n->sem_type=fe_type_error_union(&c->types,a); + return n->sem_type; + } + if (strcmp(n->a->b->text,"replace")==0) { + FeNode *value=arg ? arg->next : 0; + if(!arg || !value || value->next) + err(c,n->loc,"mem.replace requires destination and value"); + a=arg ? check_expr(s,arg) : unknown(c); + if(!a || a->kind!=FE_TYPE_REF || !a->ref_mut || + !arg->a || !lvalue_writable(s,arg->a)) + err(c,n->loc,"mem.replace destination must be a mutable place"); + b=value ? check_expr(s,value) : unknown(c); + if(a && a->kind==FE_TYPE_REF && !compatible(a->elem,b,value)) + err(c,value->loc,"mem.replace value type mismatch"); + if(value) mark_moved(s,value,b); + n->sem_type=a && a->kind==FE_TYPE_REF ? a->elem : unknown(c); + fe_type_require_replace(&c->types,n->sem_type); + return n->sem_type; + } } if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && @@ -701,7 +764,39 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; } if (n->a && n->a->kind == FE_N_MEMBER) { + FeNode *method; + FeNode *self_param; et=check_expr(s,n->a->a); + method=et && et->kind==FE_TYPE_STRUCT ? + find_method(c,et,n->a->b ? n->a->b->text : "") : 0; + if(method) { + self_param=method->a ? method->a->children : 0; + if(!self_param) { + err(c,n->loc,"method requires self parameter"); + return unknown(c); + } + a=method_type(c,self_param->a,et); + if(a->kind==FE_TYPE_REF && a->ref_mut && + !lvalue_writable(s,n->a->a)) + err(c,n->loc,"mutable method requires a mutable receiver"); + if(a->kind!=FE_TYPE_REF) mark_moved(s,n->a->a,et); + param=self_param->next; + arg=n->children; + while(param && arg) { + a=check_expr(s,arg); + b=method_type(c,param->a,et); + if(!compatible(b,a,arg) && a->kind!=FE_TYPE_UNKNOWN) + err(c,arg->loc,"method argument type mismatch"); + mark_moved(s,arg,a); + param=param->next; + arg=arg->next; + } + if(param || arg) err(c,n->loc,"wrong number of method arguments"); + n->sem_decl=method; + n->sem_type=method->b ? method_type(c,method->b,et) : + fe_type_intern(&c->types,"void"); + return n->sem_type; + } variant=et && et->kind==FE_TYPE_ENUM ? fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; arg=n->children; @@ -761,6 +856,13 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) n->sem_type=a->elem; return a->elem; } + if(a->kind==FE_TYPE_REF && a->elem && + a->elem->kind==FE_TYPE_STRUCT) { + field=fe_type_field(a->elem,n->b ? n->b->text : ""); + if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } + n->sem_type=field->type; + return field->type; + } if (a->kind == FE_TYPE_OWNED && n->b && n->b->text && strcmp(n->b->text,"^")==0) { n->sem_type=a->elem; @@ -816,6 +918,15 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) n->sem_type=base->elem; return base->elem; } + if(base && base->kind==FE_TYPE_REF && base->elem && + base->elem->kind==FE_TYPE_STRUCT) { + if(!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + field=fe_type_field(base->elem,n->b ? n->b->text : ""); + if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } + n->sem_type=field->type; + return field->type; + } if (base && base->kind == FE_TYPE_OWNED && n->b && n->b->text && strcmp(n->b->text,"^")==0) { n->sem_type=base->elem; @@ -855,12 +966,17 @@ static void check_match(FeCheckerState *s, FeNode *n) FeVariantType *variant; int seen[256]; int wildcard=0; + FeFlowSlot base[64], merged[64], current[64]; + unsigned flow_count; + int have_merged=0; unsigned i; for(i=0;i<256U;i++) seen[i]=0; value=check_expr(s,n->a); if(!value || value->kind!=FE_TYPE_ENUM) { err(s->c,n->loc,"match requires an enum value"); return; } + flow_count=flow_capture(s->scope,base,64); for(arm=n->children;arm;arm=arm->next) { FeScope *old=s->scope; + flow_restore(base,flow_count); if(arm->text && strcmp(arm->text,"_")==0) wildcard=1; else { variant=fe_type_variant(value,arm->text); @@ -885,7 +1001,19 @@ static void check_match(FeCheckerState *s, FeNode *n) if(arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); else if(arm->a) check_expr(s,arm->a); s->scope=old; + flow_capture(s->scope,current,flow_count); + if(!have_merged) { + for(i=0;ivariant_count && i<256U;i++) if(!seen[i]) err(s->c,n->loc,"non-exhaustive match"); } @@ -1073,12 +1201,12 @@ static void check_stmt(FeCheckerState *s, FeNode *n) --s->defer_depth; break; case FE_N_IF: { - FeFlowSlot base[128], left[128], right[128]; + FeFlowSlot base[64], left[64], right[64]; unsigned flow_count; a = check_expr(s, n->a); if (known(a) && a->kind != FE_TYPE_BOOL) err(c, n->loc, "if condition must be bool"); - flow_count=flow_capture(s->scope,base,128); + flow_count=flow_capture(s->scope,base,64); check_stmt(s, n->b); flow_capture(s->scope,left,flow_count); flow_restore(base,flow_count); @@ -1092,25 +1220,32 @@ static void check_stmt(FeCheckerState *s, FeNode *n) break; } case FE_N_WHILE: { - FeFlowSlot base[128], body[128]; + FeFlowSlot base[64], body[64], entry2[64]; unsigned flow_count; + unsigned i; a = check_expr(s, n->a); if (known(a) && a->kind != FE_TYPE_BOOL) err(c, n->loc, "while condition must be bool"); - flow_count=flow_capture(s->scope,base,128); + flow_count=flow_capture(s->scope,base,64); if (s->loop_depth < 255U) ++s->loop_depth; check_stmt(s, n->b); if (s->loop_depth) --s->loop_depth; flow_capture(s->scope,body,flow_count); - flow_restore(base,flow_count); - { - unsigned i; - for (i=0;iloop_depth < 255U) ++s->loop_depth; + check_stmt(s,n->b); + if (s->loop_depth) --s->loop_depth; + flow_capture(s->scope,body,flow_count); + for(i=0;ib ? method_type(c,fn->b,owner) : fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + fn->sem_type=s.ret; + for(x=fn->a ? fn->a->children : 0; x; x=x->next) { + t=method_type(c,x->a,owner); + x->sem_type=t; + add_symbol(&s,s.scope,x->text,t,0,1,1, + local_cname(c,x->text ? x->text : "arg"),x); + } + if(fn->c) check_stmt(&s,fn->c); +} + int fe_check_program(FeCheck *c) { FeCheckerState s; @@ -1189,6 +1346,15 @@ int fe_check_program(FeCheck *c) check_type_cycles(c); fe_type_layout_all(&c->types); for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { + if(n->kind==FE_N_STRUCT) { + FeNode *m; + char method_name[128]; + for(m=n->children; m; m=m->next) if(m->kind==FE_N_FN) { + sprintf(method_name,"%s_%s",n->text ? n->text : "Type", + m->text ? m->text : "method"); + m->cname=unit_cname(c,method_name); + } + } if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { t = n->a ? node_type(c, n->a) : unknown(c); add_symbol(&s, s.globals, n->text, t, 0, @@ -1221,6 +1387,13 @@ int fe_check_program(FeCheck *c) } for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) if (n->kind == FE_N_FN) check_fn(c, n, s.globals); + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) + if(n->kind==FE_N_STRUCT) { + FeNode *m; + t=fe_type_intern(&c->types,n->text); + for(m=n->children; m; m=m->next) + if(m->kind==FE_N_FN) check_method(c,m,s.globals,t); + } fe_type_layout_all(&c->types); return c->diags->errors == 0; } diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 0df0575..0a053dc 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -63,6 +63,8 @@ static void emit_one_type(FeEmitter *e, FeType *t) if (!t || t->emit_state || (t->kind != FE_TYPE_STRUCT && t->kind != FE_TYPE_ENUM && t->kind != FE_TYPE_ARRAY && t->kind != FE_TYPE_SLICE && + !(t->kind == FE_TYPE_OWNED && t->elem && + t->elem->kind == FE_TYPE_SLICE) && t->kind != FE_TYPE_ERROR_UNION) || (t->kind == FE_TYPE_ERROR_UNION && (!t->error_value || t->error_value->kind == FE_TYPE_VOID))) return; @@ -79,6 +81,15 @@ static void emit_one_type(FeEmitter *e, FeType *t) if(!t->ref_mut) fputs("const ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); fputs(";\n",e->out); fprintf(e->out,"static %s %s(%s%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n",t->cname,t->maker,t->ref_mut ? "" : "const ",fe_type_c_name(t->elem,e->pointer_bits),t->cname); + } else if(t->kind==FE_TYPE_OWNED && t->elem && + t->elem->kind==FE_TYPE_SLICE) { + FeType *item=t->elem->elem; + fputs("typedef struct { ",e->out); + fputs(fe_type_c_name(item,e->pointer_bits),e->out); + fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); + fputs(";\n",e->out); + fprintf(e->out,"static %s %s(%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n", + t->cname,t->maker,fe_type_c_name(item,e->pointer_bits),t->cname); } else if(t->kind==FE_TYPE_ERROR_UNION) { fputs(t->cname,e->out); fputs(" { unsigned short e; ",e->out); fputs(fe_type_c_name(t->error_value,e->pointer_bits),e->out); @@ -128,13 +139,21 @@ static void emit_drop_fields(FeEmitter *e, FeType *t) if (!type_needs_drop(ft)) continue; if (ft->kind==FE_TYPE_OWNED) { fputs("if (self->",e->out); fputs(t->fields[i-1].name,e->out); + if(ft->elem && ft->elem->kind==FE_TYPE_SLICE) fputs(".p",e->out); fputs(") { ",e->out); - if (ft->elem && type_needs_drop(ft->elem) && ft->elem->drop_cname) { + if(ft->elem && ft->elem->kind==FE_TYPE_SLICE) { + fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); + fputs(".p); self->",e->out); fputs(t->fields[i-1].name,e->out); + fputs(".p=0; ",e->out); + } else if (ft->elem && type_needs_drop(ft->elem) && ft->elem->drop_cname) { fprintf(e->out,"%s(self->%s); ",ft->elem->drop_cname,t->fields[i-1].name); } - fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs("); self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs("=0; }\n",e->out); + if(!(ft->elem && ft->elem->kind==FE_TYPE_SLICE)) { + fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); + fputs("); self->",e->out); fputs(t->fields[i-1].name,e->out); + fputs("=0; ",e->out); + } + fputs("}\n",e->out); } else if (ft->kind==FE_TYPE_STRUCT && ft->drop_cname) { fprintf(e->out,"%s(&self->%s);\n",ft->drop_cname,t->fields[i-1].name); } else if (ft->kind==FE_TYPE_ARRAY && ft->drop_cname) { @@ -147,7 +166,10 @@ static void emit_drop_helpers(FeEmitter *e) { FeType *t; FeNode *method; - FeNode *param; + for (t=e->check->types.types; t; t=t->next) + if(t->kind==FE_TYPE_STRUCT && (method=find_drop_method(e,t->name))!=0) + fprintf(e->out,"void %s(%s *self);\n", + cname(method,"fe_drop_method"),t->cname); for (t=e->check->types.types; t; t=t->next) if ((t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY) && type_needs_drop(t) && t->drop_cname) @@ -157,10 +179,7 @@ static void emit_drop_helpers(FeEmitter *e) fprintf(e->out,"static void %s(%s *self) {\n",t->drop_cname,t->cname); method=find_drop_method(e,t->name); if (method) { - param=method->a ? method->a->children : 0; - if (param) param->cname="self"; - if (method->c) emit_block(e,method->c); - fputc('\n',e->out); + fprintf(e->out,"%s(self);\n",cname(method,"fe_drop_method")); } emit_drop_fields(e,t); fputs("}\n",e->out); @@ -189,14 +208,29 @@ static void emit_type_helpers(FeEmitter *e) t->error_value->kind!=FE_TYPE_VOID) { fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", t->cname,t->maker,fe_type_c_name(t->error_value,e->pointer_bits),t->cname); - if (t->error_value->kind==FE_TYPE_OWNED) - fprintf(e->out,"static %s %s(void) { %s r; r.v=(%s)malloc(sizeof(%s)); r.e=r.v ? 0 : 1; return r; }\n", + if (t->error_value->kind==FE_TYPE_OWNED && + t->error_value->elem && + t->error_value->elem->kind==FE_TYPE_SLICE) { + FeType *item=t->error_value->elem->elem; + fprintf(e->out,"static %s %s(unsigned long n) { %s r; r.v.p=(%s*)malloc(sizeof(%s)*n); r.v.n=n; r.e=(r.v.p || !n) ? 0 : 1; return r; }\n", t->cname,t->alloc_cname,t->cname, + fe_type_c_name(item,e->pointer_bits), + fe_type_c_name(item,e->pointer_bits)); + } else if (t->error_value->kind==FE_TYPE_OWNED) { + fprintf(e->out,"static %s %s(%s v) { %s r; r.v=(%s)malloc(sizeof(%s)); if(r.v) *r.v=v; r.e=r.v ? 0 : 1; return r; }\n", + t->cname,t->alloc_cname, + fe_type_c_name(t->error_value->elem,e->pointer_bits),t->cname, fe_type_c_name(t->error_value,e->pointer_bits), fe_type_c_name(t->error_value->elem,e->pointer_bits)); + } } } for(t=e->check->types.types;t;t=t->next) { + if(t->replace_cname) { + const char *ct=fe_type_c_name(t,e->pointer_bits); + fprintf(e->out,"static %s %s(%s *dst, %s value) { %s old=*dst; *dst=value; return old; }\n", + ct,t->replace_cname,ct,ct,ct); + } if(t->kind==FE_TYPE_STRUCT && t->maker) { fprintf(e->out,"static %s %s(",t->cname,t->maker); for(i=0;ifield_count;i++) { if(i) fputs(", ",e->out); fputs(fe_type_c_name(t->fields[i].type,e->pointer_bits),e->out); fprintf(e->out," p%u",i); } @@ -599,6 +633,10 @@ static void emit_lvalue(FeEmitter *e, FeNode *n) } else if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OWNED && n->b && n->b->text && strcmp(n->b->text,"^")==0) { fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); + } else if(n->a && n->a->sem_type && + n->a->sem_type->kind==FE_TYPE_REF) { + emit_expr(e,n->a); fputs("->",e->out); + fputs(n->b ? n->b->text : "member",e->out); } else { emit_lvalue(e,n->a); fputc('.',e->out); fputs(n->b ? n->b->text : "member",e->out); } return; } @@ -612,10 +650,17 @@ static void emit_lvalue(FeEmitter *e, FeNode *n) static void emit_destroy_expr(FeEmitter *e, FeNode *n) { - fputs("(free(",e->out); emit_expr(e,n); fputs(")",e->out); + fputs("(free(",e->out); emit_expr(e,n); + if(n && n->sem_type && n->sem_type->kind==FE_TYPE_OWNED && + n->sem_type->elem && n->sem_type->elem->kind==FE_TYPE_SLICE) + fputs(".p",e->out); + fputs(")",e->out); if (n && n->kind==FE_N_IDENT) { fputs(", ",e->out); emit_lvalue(e,n); - fputs("=0, fe_live_",e->out); fputs(cname(n,"owned"),e->out); + if(n->sem_type && n->sem_type->elem && + n->sem_type->elem->kind==FE_TYPE_SLICE) fputs(".p=0",e->out); + else fputs("=0",e->out); + fputs(", fe_live_",e->out); fputs(cname(n,"owned"),e->out); fputs("=0",e->out); } else { fputs(", ",e->out); emit_lvalue(e,n); fputs("=0",e->out); @@ -698,7 +743,12 @@ static void emit_expr(FeEmitter *e, FeNode *n) } switch (n->kind) { case FE_N_IDENT: - fputs(cname(n, "fe_missing"), e->out); + if((n->flags & 0x100U) && n->sem_type && + type_needs_drop(n->sem_type)) { + fputs("(fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("=0, ",e->out); fputs(cname(n,"fe_missing"),e->out); + fputc(')',e->out); + } else fputs(cname(n, "fe_missing"), e->out); break; case FE_N_LITERAL: if (n->text && strcmp(n->text, "true") == 0) fputs("1", e->out); @@ -760,7 +810,7 @@ static void emit_expr(FeEmitter *e, FeNode *n) if (strcmp(op, "not") == 0) fputs("(!", e->out); else { fputc('(', e->out); - fputs(op, e->out); + fputs(strcmp(op,"&mut")==0 ? "&" : op, e->out); } emit_expr(e, n->a); fputc(')', e->out); @@ -801,19 +851,55 @@ static void emit_expr(FeEmitter *e, FeNode *n) n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && strcmp(n->a->b->text,"create")==0 && - n->children && n->children->kind==FE_N_IDENT) { - FeType *created=fe_type_intern(&e->check->types,n->children->text); + n->children) { + FeType *created=n->children->sem_type; FeType *owned=fe_type_owned(&e->check->types,created); FeType *result=fe_type_error_union(&e->check->types,owned); if (result->alloc_cname) fputs(result->alloc_cname,e->out); else fputs("fe_bad_alloc",e->out); - fputs("()",e->out); + fputc('(',e->out); emit_expr(e,n->children); fputc(')',e->out); + special=1; + } + else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && + n->a->b->text && strcmp(n->a->b->text,"alloc_slice")==0 && + n->children && n->children->next) { + FeType *result=n->sem_type; + if(result && result->alloc_cname) fputs(result->alloc_cname,e->out); + else fputs("fe_bad_slice_alloc",e->out); + fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out); + special=1; + } + else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && + n->a->b->text && strcmp(n->a->b->text,"replace")==0 && + n->children && n->children->next && n->sem_type) { + fputs(n->sem_type->replace_cname ? n->sem_type->replace_cname : + "fe_bad_replace",e->out); + fputc('(',e->out); emit_expr(e,n->children); fputs(", ",e->out); + emit_expr(e,n->children->next); fputc(')',e->out); special=1; } else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && strcmp(n->a->b->text,"null_writer")==0) { fputs("fe_m4_null_writer()",e->out); special=1; } + else if(n->a && n->a->kind==FE_N_MEMBER && n->sem_decl && + n->sem_decl->kind==FE_N_FN) { + FeNode *mp=n->sem_decl->a ? n->sem_decl->a->children : 0; + FeNode *ma; + fputs(cname(n->sem_decl,"fe_method"),e->out); fputc('(',e->out); + if(mp && mp->sem_type && mp->sem_type->kind==FE_TYPE_REF) { + fputc('&',e->out); emit_lvalue(e,n->a->a); + } else emit_expr(e,n->a->a); + for(ma=n->children; ma; ma=ma->next) { + fputs(", ",e->out); emit_expr(e,ma); + } + fputc(')',e->out); + special=1; + } else if(!n->a && n->text && strcmp(n->text,"@size_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%lu",fe_type_size(fe_type_intern(&e->check->types,n->children->text))); special=1; } else if(!n->a && n->text && strcmp(n->text,"@align_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%u",fe_type_align(fe_type_intern(&e->check->types,n->children->text))); special=1; } else if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM) { @@ -860,7 +946,10 @@ static void emit_expr(FeEmitter *e, FeNode *n) n->b && n->b->text && strcmp(n->b->text,"^")==0) { fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); } else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_ENUM) { v=fe_type_variant(n->a->sem_type,n->b ? n->b->text : ""); if(v) fputs(v->maker,e->out); else fputs("0",e->out); if(v)fputs("()",e->out); } - else { emit_expr(e, n->a); fputc('.', e->out); if (n->b) fputs(n->b->text ? n->b->text : "member", e->out); } + else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF) { + emit_expr(e,n->a); fputs("->",e->out); + if(n->b) fputs(n->b->text ? n->b->text : "member",e->out); + } else { emit_expr(e, n->a); fputc('.', e->out); if (n->b) fputs(n->b->text ? n->b->text : "member", e->out); } break; } default: @@ -887,7 +976,7 @@ static void emit_decl(FeEmitter *e, FeNode *n) } fputs(";\n", e->out); if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && - n->sem_type->kind==FE_TYPE_OWNED) { + type_needs_drop(n->sem_type)) { pad(e); fputs("unsigned char fe_live_",e->out); fputs(cname(n,"owned"),e->out); fputs("=0;\n",e->out); } @@ -895,7 +984,7 @@ static void emit_decl(FeEmitter *e, FeNode *n) static void emit_owned_live(FeEmitter *e, FeNode *n, int value) { - if (n && n->sem_type && n->sem_type->kind==FE_TYPE_OWNED) { + if (n && n->sem_type && type_needs_drop(n->sem_type)) { pad(e); fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); fprintf(e->out,"=%d;\n",value); } @@ -909,14 +998,21 @@ static void emit_value_drop(FeEmitter *e, FeNode *n) if (t->kind==FE_TYPE_OWNED) { pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"owned"),e->out); fputs(") { ",e->out); - if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) { + if(t->elem && t->elem->kind==FE_TYPE_SLICE) { + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); + fputs(".p); ",e->out); + } else if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) { fprintf(e->out,"%s(%s); ",t->elem->drop_cname,cname(n,"owned")); + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); fputs("); ",e->out); + } else { + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); fputs("); ",e->out); } - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); - fputs("); fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); fputs("=0; }\n",e->out); } else if (t->drop_cname) { - pad(e); fprintf(e->out,"%s(&%s);\n",t->drop_cname,cname(n,"local")); + pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"local"),e->out); + fprintf(e->out,") { %s(&%s); fe_live_",t->drop_cname,cname(n,"local")); + fputs(cname(n,"local"),e->out); fputs("=0; }\n",e->out); } } @@ -1006,7 +1102,7 @@ static void emit_block(FeEmitter *e, FeNode *n) if (e->current_fn && e->current_fn->c==n && e->current_fn->a) { FeNode *param; for (param=e->current_fn->a->children; param; param=param->next) - if (param->sem_type && param->sem_type->kind==FE_TYPE_OWNED) { + if (param->sem_type && type_needs_drop(param->sem_type)) { pad(e); fputs("unsigned char fe_live_",e->out); fputs(cname(param,"owned"),e->out); fputs("=1;\n",e->out); } @@ -1126,12 +1222,26 @@ static void emit_stmt(FeEmitter *e, FeNode *n) n->a->sem_type->kind==FE_TYPE_OWNED) { pad(e); fputs("if (fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); fputs(") { ",e->out); - if (n->a->sem_type->elem && type_needs_drop(n->a->sem_type->elem) && + if(n->a->sem_type->elem && + n->a->sem_type->elem->kind==FE_TYPE_SLICE) { + fputs("free(",e->out); fputs(cname(n->a,"owned"),e->out); + fputs(".p); ",e->out); + } else if (n->a->sem_type->elem && type_needs_drop(n->a->sem_type->elem) && n->a->sem_type->elem->drop_cname) fprintf(e->out,"%s(%s); ",n->a->sem_type->elem->drop_cname,cname(n->a,"owned")); - fputs("free(",e->out); fputs(cname(n->a,"owned"),e->out); - fputs("); fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); + if(!(n->a->sem_type->elem && + n->a->sem_type->elem->kind==FE_TYPE_SLICE)) { + fputs("free(",e->out); fputs(cname(n->a,"owned"),e->out); fputs("); ",e->out); + } + fputs("fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); fputs("=0; }\n",e->out); + } else if(n->a && n->a->kind==FE_N_IDENT && n->a->sem_type && + type_needs_drop(n->a->sem_type) && + n->a->sem_type->drop_cname) { + pad(e); fputs("if (fe_live_",e->out); fputs(cname(n->a,"local"),e->out); + fprintf(e->out,") { %s(&%s); fe_live_", + n->a->sem_type->drop_cname,cname(n->a,"local")); + fputs(cname(n->a,"local"),e->out); fputs("=0; }\n",e->out); } pad(e); emit_lvalue(e, n->a); @@ -1153,12 +1263,12 @@ static void emit_stmt(FeEmitter *e, FeNode *n) pad(e); if (n->a && n->a->kind==FE_N_UNARY && n->a->text && strcmp(n->a->text,"try")==0 && n->a->a) { - fputs("if ((fe_m4_error = ",e->out); + fputs("if ((fe_error_temp = ",e->out); emit_expr(e,n->a->a); fputs(") != 0) {\n",e->out); ++e->indent; emit_cleanup_all(e); - pad(e); fputs("return fe_m4_error;\n",e->out); + pad(e); fputs("return fe_error_temp;\n",e->out); --e->indent; pad(e); fputs("}\n",e->out); } else { @@ -1275,7 +1385,8 @@ static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) if (!p) fputs("void", e->out); while (p) { if (p != fn->a->children) fputs(", ", e->out); - fputs(ctype(e, p->a), e->out); + fputs(p->sem_type ? fe_type_c_name(p->sem_type,e->pointer_bits) : + ctype(e,p->a),e->out); fputc(' ', e->out); fputs(cname(p, "fe_arg"), e->out); p = p->next; @@ -1343,7 +1454,7 @@ void fe_emit_c_program(FeEmitter *e) fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); else fputs("typedef char fe_assert_usize[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); - fputs("static void fe_trap_bounds(void) { abort(); }\n\n", e->out); + fputs("static void fe_trap_bounds(void) { abort(); }\nstatic unsigned short fe_error_temp;\n\n", e->out); emit_type_defs(e); if (need_m4) emit_m4_runtime(e); emit_type_helpers(e); @@ -1373,10 +1484,24 @@ void fe_emit_c_program(FeEmitter *e) emit_fn(e, n, 1); if (n->text && strcmp(n->text, "main") == 0) main_fn = n; } + for (n = e->check->ast->root ? e->check->ast->root->children : 0; + n; n = n->next) if(n->kind==FE_N_STRUCT) { + FeNode *m; + for(m=n->children; m; m=m->next) + if(m->kind==FE_N_FN) + emit_fn(e,m,1); + } fputc('\n', e->out); for (n = e->check->ast->root ? e->check->ast->root->children : 0; n; n = n->next) if (n->kind == FE_N_FN) emit_fn(e, n, 0); + for (n = e->check->ast->root ? e->check->ast->root->children : 0; + n; n = n->next) if(n->kind==FE_N_STRUCT) { + FeNode *m; + for(m=n->children; m; m=m->next) + if(m->kind==FE_N_FN) + emit_fn(e,m,0); + } if (main_fn) { fputc('\n', e->out); emit_main_wrapper(e, main_fn); diff --git a/fec/src/types.c b/fec/src/types.c index c471dcf..542df00 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -21,6 +21,7 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->tail_slicer = 0; t->drop_cname = 0; t->alloc_cname = 0; + t->replace_cname = 0; t->bits = 0; t->is_unsigned = 0; t->packed = 0; @@ -200,6 +201,10 @@ FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem) if(t->kind==FE_TYPE_UNKNOWN) { t->kind=FE_TYPE_OWNED; t->elem=elem; + if(elem && elem->kind==FE_TYPE_SLICE) { + t->cname=generated_name(ctx,"fe_owned_slice_","type"); + t->maker=generated_name(ctx,"fe_make_owned_slice_","type"); + } } return t; } @@ -222,6 +227,12 @@ FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value) return t; } +void fe_type_require_replace(FeTypeCtx *ctx, FeType *type) +{ + if(type && !type->replace_cname) + type->replace_cname=generated_name(ctx,"fe_replace_","type"); +} + FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) { FeType *t; @@ -410,7 +421,9 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) t->cycle_state = 2; return; } if (t->kind == FE_TYPE_OWNED) { - t->size = ctx->pointer_bits == 16 ? 2UL : 4UL; + t->size = t->elem && t->elem->kind==FE_TYPE_SLICE ? + (ctx->pointer_bits == 16 ? 4UL : 8UL) : + (ctx->pointer_bits == 16 ? 2UL : 4UL); t->align = ctx->pointer_bits == 16 ? 1U : 4U; t->cycle_state = 2; return; } diff --git a/fec/src/types.h b/fec/src/types.h index fae31ae..1974a46 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -39,6 +39,7 @@ struct FeType { char *tail_slicer; char *drop_cname; char *alloc_cname; + char *replace_cname; unsigned bits; int is_unsigned; int packed; @@ -77,6 +78,7 @@ FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable); FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value); +void fe_type_require_replace(FeTypeCtx *ctx, FeType *type); FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed); FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node); FeType *fe_type_declare_error(FeTypeCtx *ctx, const FeNode *node); diff --git a/fec/std/mem.fe b/fec/std/mem.fe index cec54f5..4fe0cd7 100644 --- a/fec/std/mem.fe +++ b/fec/std/mem.fe @@ -1,8 +1,10 @@ unit mem; -pub fn create(T: type) -> !^T; +pub fn create(value: T) -> !^T; pub fn destroy(p: *void); -pub fn copy(dst: []u8, src: []u8); +pub fn alloc_slice(T: type, n: usize) -> !^[]T; +pub fn replace(dst: &mut T, value: T) -> T; +pub fn copy(dst: []mut u8, src: []u8); pub struct Arena { ptr: *void, pub fn init() -> Arena { return Arena{ ptr: null }; } diff --git a/fec/test-dos.bat b/fec/test-dos.bat index 60acb3e..bdd2d15 100644 --- a/fec/test-dos.bat +++ b/fec/test-dos.bat @@ -259,14 +259,20 @@ fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DBL.FE -o TESTS\M5\BAD-DBL.C > nul if not errorlevel 1 goto test_fail fec.exe --target=bits32 --emit-c TESTS\M5\BAD-COND.FE -o TESTS\M5\BAD-COND.C > nul if not errorlevel 1 goto test_fail -if exist TESTS\M5\RUNTIME-G.C del TESTS\M5\RUNTIME-G.C +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-PROJ.FE -o TESTS\M5\BAD-PROJ.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-CLOS.FE -o TESTS\M5\BAD-CLOS.C > nul +if not errorlevel 1 goto test_fail +fec.exe --target=bits32 --emit-c TESTS\M5\BAD-LOOP.FE -o TESTS\M5\BAD-LOOP.C > nul +if not errorlevel 1 goto test_fail +if exist TESTS\M5\RUNT-G.C del TESTS\M5\RUNT-G.C if exist TESTS\M5\RUNTIME.O del TESTS\M5\RUNTIME.O if exist TESTS\M5\RUNTIME.EXE del TESTS\M5\RUNTIME.EXE -fec.exe --target=bits32 --emit-c TESTS\M5\RUNTIME.FE -o TESTS\M5\RUNTIME-G.C > nul +fec.exe --target=bits32 --emit-c TESTS\M5\RUNTIME.FE -o TESTS\M5\RUNT-G.C > nul if errorlevel 1 goto test_fail rem Compile generated source and the C89 runtime harness in one WCL386 invocation rem so both objects use the same DOS/4GW startup and runtime library. -wcl386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\M5\RUNTIME.EXE TESTS\M5\RUNTIME-G.C TESTS\M5\RUNTIME.C +wcl386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\M5\RUNTIME.EXE TESTS\M5\RUNT-G.C TESTS\M5\RUNTIME.C if errorlevel 1 goto test_fail TESTS\M5\RUNTIME.EXE if errorlevel 1 goto test_fail diff --git a/fec/tests/m5/bad-clos.fe b/fec/tests/m5/bad-clos.fe new file mode 100644 index 0000000..530696b --- /dev/null +++ b/fec/tests/m5/bad-clos.fe @@ -0,0 +1,13 @@ +unit m5_bad_consuming_close; + +struct FileLike { + handle: i32, + fn close(self: Self) -> !void { self.handle = 0; } + fn drop(self: &mut Self) { self.handle = 0; } +} + +fn bad() -> !void { + let file: FileLike = FileLike{ handle: 7 }; + try file.close(); + file.close(); +} diff --git a/fec/tests/m5/bad-loop.fe b/fec/tests/m5/bad-loop.fe new file mode 100644 index 0000000..091d253 --- /dev/null +++ b/fec/tests/m5/bad-loop.fe @@ -0,0 +1,7 @@ +unit m5_bad_loop_move; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(p: ^i32, again: bool) -> void { + while again { take(p); } +} diff --git a/fec/tests/m5/bad-proj.fe b/fec/tests/m5/bad-proj.fe new file mode 100644 index 0000000..bf928ea --- /dev/null +++ b/fec/tests/m5/bad-proj.fe @@ -0,0 +1,8 @@ +unit m5_bad_projection_move; + +struct Holder { p: ^i32 } +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(h: Holder) -> void { + take(h.p); +} diff --git a/fec/tests/m5/owned.fe b/fec/tests/m5/owned.fe index 7f7a553..e3f44f6 100644 --- a/fec/tests/m5/owned.fe +++ b/fec/tests/m5/owned.fe @@ -1,8 +1,8 @@ unit m5_owned; fn main() -> void { - var p: ^i32 = try mem.create(i32); - p = try mem.create(i32); + var p: ^i32 = try mem.create(0); + p = try mem.create(0); p.^ = 7; let value: i32 = p.^; defer { mem.destroy(p); } diff --git a/fec/tests/m5/runtime.c b/fec/tests/m5/runtime.c index 04d5c10..43e51e0 100644 --- a/fec/tests/m5/runtime.c +++ b/fec/tests/m5/runtime.c @@ -4,21 +4,43 @@ #undef malloc #undef free +extern void *malloc(size_t size); +extern void free(void *p); + extern long fe_m5_runtime_run(long mode); -extern void fe_m5_runtime_conditional(unsigned char flag); -extern void fe_m5_runtime_argument_cleanup(void); +extern unsigned short fe_m5_runtime_conditional(unsigned char flag); +extern unsigned short fe_m5_runtime_argument_cleanup(void); +extern unsigned short fe_m5_runtime_owned_slice(unsigned long n); +extern unsigned short fe_m5_runtime_replace_field(void); +extern unsigned short fe_m5_runtime_loop_cleanup(void); +extern unsigned short fe_m5_runtime_try_cleanup(void); +extern unsigned short fe_m5_runtime_field_order(void); +extern unsigned short fe_m5_runtime_defer_order(void); +extern unsigned short fe_m5_runtime_match_cleanup(unsigned char flag); +extern unsigned short fe_m5_runtime_close_once(void); +extern unsigned short fe_m5_runtime_reassign_struct(void); static void *live_ptrs[64]; static unsigned live_count; static unsigned alloc_count; static unsigned free_count; static unsigned double_free_count; +static long fail_after = -1; +static unsigned malloc_attempts; +static int track_order; +static void *order_ptrs[2]; +static unsigned order_allocs; +static unsigned order_frees; +static unsigned order_bad; void *m5_malloc(size_t size) { - void *p = malloc(size); + void *p; + if (fail_after >= 0 && (long)malloc_attempts++ == fail_after) return 0; + p = malloc(size); if (p && live_count < 64) live_ptrs[live_count++] = p; if (p) ++alloc_count; + if (p && track_order && order_allocs < 2) order_ptrs[order_allocs++] = p; return p; } @@ -28,6 +50,9 @@ void m5_free(void *p) if (!p) return; for (i = 0; i < live_count; ++i) { if (live_ptrs[i] == p) { + if (track_order && order_frees < 2 && + p != order_ptrs[1-order_frees]) ++order_bad; + if (track_order && order_frees < 2) ++order_frees; live_ptrs[i] = live_ptrs[--live_count]; ++free_count; free(p); @@ -42,11 +67,32 @@ int main(void) if (fe_m5_runtime_run(0) != 0) return 1; if (fe_m5_runtime_run(1) != 9) return 2; if (fe_m5_runtime_run(2) != 0) return 3; - fe_m5_runtime_conditional(0); - fe_m5_runtime_conditional(1); - fe_m5_runtime_argument_cleanup(); - if (double_free_count != 0) return 4; - if (live_count != 0) return 5; - if (alloc_count != free_count) return 6; + if (fe_m5_runtime_conditional(0) != 0) return 4; + if (fe_m5_runtime_conditional(1) != 0) return 5; + if (fe_m5_runtime_argument_cleanup() != 0) return 6; + if (fe_m5_runtime_owned_slice(17) != 0) return 7; + if (fe_m5_runtime_replace_field() != 0) return 8; + if (fe_m5_runtime_loop_cleanup() != 0) return 9; + fail_after=1; + malloc_attempts=0; + if (fe_m5_runtime_try_cleanup() == 0) return 10; + fail_after=-1; + track_order=1; + order_allocs=order_frees=order_bad=0; + if (fe_m5_runtime_field_order() != 0) return 11; + track_order=0; + if (order_allocs != 2 || order_frees != 2 || order_bad != 0) return 12; + track_order=1; + order_allocs=order_frees=order_bad=0; + if (fe_m5_runtime_defer_order() != 0) return 13; + track_order=0; + if (order_allocs != 2 || order_frees != 2 || order_bad != 0) return 14; + if (fe_m5_runtime_match_cleanup(0) != 0) return 15; + if (fe_m5_runtime_match_cleanup(1) != 0) return 16; + if (fe_m5_runtime_close_once() != 0) return 17; + if (fe_m5_runtime_reassign_struct() != 0) return 18; + if (double_free_count != 0) return 19; + if (live_count != 0) return 20; + if (alloc_count != free_count) return 21; return 0; } diff --git a/fec/tests/m5/runtime.fe b/fec/tests/m5/runtime.fe index 7df03b6..38a35da 100644 --- a/fec/tests/m5/runtime.fe +++ b/fec/tests/m5/runtime.fe @@ -3,11 +3,11 @@ unit m5_runtime; fn take(p: ^i32) -> void { mem.destroy(p); } pub fn run(mode: i32) -> i32 { - var p: ^i32 = try mem.create(i32); + var p: ^i32 = try mem.create(0); defer { mem.destroy(p); } p.^ = 7; if mode == 1 { - p = try mem.create(i32); + p = try mem.create(0); p.^ = 9; return p.^; } @@ -16,12 +16,85 @@ pub fn run(mode: i32) -> i32 { return p.^ - 7; } -pub fn conditional(flag: bool) -> void { - var p: ^i32 = try mem.create(i32); +pub fn conditional(flag: bool) -> !void { + var p: ^i32 = try mem.create(0); if flag { take(p); } } -pub fn argument_cleanup() -> void { - let p: ^i32 = try mem.create(i32); +pub fn argument_cleanup() -> !void { + let p: ^i32 = try mem.create(0); take(p); } + +pub fn owned_slice(n: usize) -> !void { + let bytes: ^[]u8 = try mem.alloc_slice(u8, n); +} + +struct Holder { p: ^i32 } + +pub fn replace_field() -> !void { + let first: ^i32 = try mem.create(1); + var h: Holder = Holder{ p: first }; + let second: ^i32 = try mem.create(2); + let old: ^i32 = mem.replace(&mut h.p, second); + mem.destroy(old); +} + +pub fn loop_cleanup() -> !void { + var i: i32 = 0; + while i < 2 { + let p: ^i32 = try mem.create(i); + i += 1; + if i == 1 { continue; } + break; + } +} + +pub fn try_cleanup() -> !void { + let first: ^i32 = try mem.create(1); + let second: ^i32 = try mem.create(2); +} + +struct PairOwners { first: ^i32, second: ^i32 } + +pub fn field_order() -> !void { + let first: ^i32 = try mem.create(1); + let second: ^i32 = try mem.create(2); + let pair: PairOwners = PairOwners{ first: first, second: second }; +} + +pub fn defer_order() -> !void { + let first: ^i32 = try mem.create(1); + defer { mem.destroy(first); } + let second: ^i32 = try mem.create(2); +} + +enum Choice { A, B } + +pub fn match_cleanup(flag: bool) -> !void { + var choice: Choice = Choice.A; + if flag { choice = Choice.B; } + let p: ^i32 = try mem.create(1); + match choice { + A => { take(p); } + B => { take(p); } + } +} + +struct FileLike { + handle: i32, + fn close(self: Self) -> !void { self.handle = 0; } + fn drop(self: &mut Self) { self.handle = 0; } +} + +pub fn close_once() -> !void { + let file: FileLike = FileLike{ handle: 7 }; + try file.close(); +} + +pub fn reassign_struct() -> !void { + let first: ^i32 = try mem.create(1); + var owner: Holder = Holder{ p: first }; + let second: ^i32 = try mem.create(2); + owner = Holder{ p: second }; +} From 83ce26c20984e65ef7aa20b932657a3ed943ba24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 18:50:19 +0900 Subject: [PATCH 038/184] add minimal Zed syntax highlighting for Ferro (#2) * add minimal Ferro tree-sitter grammar * add temporary tree-sitter generation workflow * generate Ferro tree-sitter parser * drop custom grammar in favor of Rust grammar reuse * remove temporary tree-sitter generation workflow * add Zed extension manifest for Ferro * configure Ferro files for Zed * add Ferro syntax highlighting queries * remove generated custom tree-sitter parser * remove generated custom tree-sitter node types * tighten Ferro Zed highlight queries * add minimal Ferro tree-sitter grammar * point Zed at Ferro grammar * refresh Ferro highlights --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- editors/zed/extension.toml | 11 + editors/zed/languages/ferro/config.toml | 6 + editors/zed/languages/ferro/highlights.scm | 12 + grammar.js | 102 ++ src/node-types.json | 121 ++ src/parser.c | 1744 ++++++++++++++++++++ src/tree_sitter/parser.h | 55 + 7 files changed, 2051 insertions(+) create mode 100644 editors/zed/extension.toml create mode 100644 editors/zed/languages/ferro/config.toml create mode 100644 editors/zed/languages/ferro/highlights.scm create mode 100644 grammar.js create mode 100644 src/node-types.json create mode 100644 src/parser.c create mode 100644 src/tree_sitter/parser.h diff --git a/editors/zed/extension.toml b/editors/zed/extension.toml new file mode 100644 index 0000000..5a28ded --- /dev/null +++ b/editors/zed/extension.toml @@ -0,0 +1,11 @@ +id = "ferro" +name = "Ferro" +version = "0.0.1" +schema_version = 1 +authors = ["sebastianrcnt"] +description = "Ferro syntax highlighting for Zed" +repository = "https://github.com/sebastianrcnt/doslang" + +[grammars.ferro] +repository = "https://github.com/sebastianrcnt/doslang" +rev = "40316bb34f5acbaf34979cdf2dce296ae752d81d" diff --git a/editors/zed/languages/ferro/config.toml b/editors/zed/languages/ferro/config.toml new file mode 100644 index 0000000..be09be8 --- /dev/null +++ b/editors/zed/languages/ferro/config.toml @@ -0,0 +1,6 @@ +name = "Ferro" +grammar = "ferro" +path_suffixes = ["fe"] +line_comments = ["// "] +tab_size = 4 +hard_tabs = false diff --git a/editors/zed/languages/ferro/highlights.scm b/editors/zed/languages/ferro/highlights.scm new file mode 100644 index 0000000..4dcca4b --- /dev/null +++ b/editors/zed/languages/ferro/highlights.scm @@ -0,0 +1,12 @@ +; Ferro syntax highlighting for Zed. + +(line_comment) @comment +(block_comment) @comment +(string_literal) @string +(char_literal) @string +(integer_literal) @number +(builtin) @function.builtin +(builtin_type) @type.builtin +(keyword) @keyword +(operator) @operator +(punctuation) @punctuation.delimiter diff --git a/grammar.js b/grammar.js new file mode 100644 index 0000000..910bb6e --- /dev/null +++ b/grammar.js @@ -0,0 +1,102 @@ +// Minimal Tree-sitter grammar for Ferro syntax highlighting in Zed. +// This intentionally models lexical structure rather than full Ferro semantics. + +module.exports = grammar({ + name: "ferro", + + extras: $ => [ + /[\s\uFEFF\u2060\u200B]/, + ], + + rules: { + source_file: $ => repeat(choice( + $.line_comment, + $.block_comment, + $.string_literal, + $.char_literal, + $.integer_literal, + $.builtin, + $.builtin_type, + $.keyword, + $.identifier, + $.operator, + $.punctuation, + )), + + line_comment: _ => token(seq("//", /[^\n]*/)), + + // Ferro block comments may nest. Keeping this rule recursive makes syntax + // highlighting follow the compiler lexer instead of flattening nested /* */. + block_comment: $ => seq( + "/*", + repeat(choice( + $.block_comment, + /[^*/]+/, + /\*[^/]/, + /\/[^*]/, + )), + "*/", + ), + + string_literal: _ => token(seq( + '"', + repeat(choice( + /[^"\\\n\r]/, + /\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|[nrt\\'"0])/, + )), + '"', + )), + + char_literal: _ => token(seq( + "'", + choice( + /[^'\\\n\r]/, + /\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|[nrt\\'"0])/, + ), + "'", + )), + + integer_literal: _ => token(prec(2, choice( + /0[xX][0-9A-Fa-f_]+/, + /0[bB][01_]+/, + /0[oO][0-7_]+/, + /[0-9][0-9_]*/, + ))), + + builtin: _ => token(prec(3, /@[A-Za-z_][A-Za-z0-9_]*/)), + + builtin_type: _ => token(prec(3, choice( + "i8", "i16", "i32", + "u8", "u16", "u32", + "usize", "isize", + "bool", "char", "void", "str", + ))), + + keyword: _ => token(prec(3, choice( + "unit", "import", "pub", "fn", "struct", "packed", "enum", "error", + "const", "static", "var", "let", "mut", + "if", "else", "while", "for", "in", "match", + "return", "break", "continue", "defer", + "unsafe", "critical", "shared", "atomic", "comptime", "asm", + "try", "catch", "orelse", "as", "extern", + "interrupt", "interrupt_safe", "far", + "true", "false", "null", "undefined", + "self", "Self", "type", + "and", "or", "not", + ))), + + identifier: _ => /[A-Za-z_][A-Za-z0-9_]*/, + + operator: _ => token(choice( + "<<=", ">>=", + "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", + "==", "!=", "<=", ">=", "<<", ">>", "->", "=>", "..", + "+%", "-%", "*%", + "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", "^", "~", "!", "?", + )), + + punctuation: _ => token(choice( + "(", ")", "{", "}", "[", "]", ",", ";", ":", ".", + )), + }, +}); diff --git a/src/node-types.json b/src/node-types.json new file mode 100644 index 0000000..ae9616e --- /dev/null +++ b/src/node-types.json @@ -0,0 +1,121 @@ +[ + { + "type": "block_comment", + "named": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "block_comment", + "named": true + } + ] + } + }, + { + "type": "source_file", + "named": true, + "root": true, + "fields": {}, + "children": { + "multiple": true, + "required": false, + "types": [ + { + "type": "block_comment", + "named": true + }, + { + "type": "builtin", + "named": true + }, + { + "type": "builtin_type", + "named": true + }, + { + "type": "char_literal", + "named": true + }, + { + "type": "identifier", + "named": true + }, + { + "type": "integer_literal", + "named": true + }, + { + "type": "keyword", + "named": true + }, + { + "type": "line_comment", + "named": true + }, + { + "type": "operator", + "named": true + }, + { + "type": "punctuation", + "named": true + }, + { + "type": "string_literal", + "named": true + } + ] + } + }, + { + "type": "*/", + "named": false + }, + { + "type": "/*", + "named": false + }, + { + "type": "builtin", + "named": true + }, + { + "type": "builtin_type", + "named": true + }, + { + "type": "char_literal", + "named": true + }, + { + "type": "identifier", + "named": true + }, + { + "type": "integer_literal", + "named": true + }, + { + "type": "keyword", + "named": true + }, + { + "type": "line_comment", + "named": true + }, + { + "type": "operator", + "named": true + }, + { + "type": "punctuation", + "named": true + }, + { + "type": "string_literal", + "named": true + } +] \ No newline at end of file diff --git a/src/parser.c b/src/parser.c new file mode 100644 index 0000000..9173d00 --- /dev/null +++ b/src/parser.c @@ -0,0 +1,1744 @@ +/* Automatically @generated by tree-sitter */ + +#include "tree_sitter/parser.h" + +#if defined(__GNUC__) || defined(__clang__) +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" +#endif + +#define LANGUAGE_VERSION 14 +#define STATE_COUNT 14 +#define LARGE_STATE_COUNT 6 +#define SYMBOL_COUNT 20 +#define ALIAS_COUNT 0 +#define TOKEN_COUNT 16 +#define EXTERNAL_TOKEN_COUNT 0 +#define FIELD_COUNT 0 +#define MAX_ALIAS_SEQUENCE_LENGTH 3 +#define MAX_RESERVED_WORD_SET_SIZE 0 +#define PRODUCTION_ID_COUNT 1 +#define SUPERTYPE_COUNT 0 + +enum ts_symbol_identifiers { + sym_line_comment = 1, + anon_sym_SLASH_STAR = 2, + aux_sym_block_comment_token1 = 3, + aux_sym_block_comment_token2 = 4, + aux_sym_block_comment_token3 = 5, + anon_sym_STAR_SLASH = 6, + sym_string_literal = 7, + sym_char_literal = 8, + sym_integer_literal = 9, + sym_builtin = 10, + sym_builtin_type = 11, + sym_keyword = 12, + sym_identifier = 13, + sym_operator = 14, + sym_punctuation = 15, + sym_source_file = 16, + sym_block_comment = 17, + aux_sym_source_file_repeat1 = 18, + aux_sym_block_comment_repeat1 = 19, +}; + +static const char * const ts_symbol_names[] = { + [ts_builtin_sym_end] = "end", + [sym_line_comment] = "line_comment", + [anon_sym_SLASH_STAR] = "/*", + [aux_sym_block_comment_token1] = "block_comment_token1", + [aux_sym_block_comment_token2] = "block_comment_token2", + [aux_sym_block_comment_token3] = "block_comment_token3", + [anon_sym_STAR_SLASH] = "*/", + [sym_string_literal] = "string_literal", + [sym_char_literal] = "char_literal", + [sym_integer_literal] = "integer_literal", + [sym_builtin] = "builtin", + [sym_builtin_type] = "builtin_type", + [sym_keyword] = "keyword", + [sym_identifier] = "identifier", + [sym_operator] = "operator", + [sym_punctuation] = "punctuation", + [sym_source_file] = "source_file", + [sym_block_comment] = "block_comment", + [aux_sym_source_file_repeat1] = "source_file_repeat1", + [aux_sym_block_comment_repeat1] = "block_comment_repeat1", +}; + +static const TSSymbol ts_symbol_map[] = { + [ts_builtin_sym_end] = ts_builtin_sym_end, + [sym_line_comment] = sym_line_comment, + [anon_sym_SLASH_STAR] = anon_sym_SLASH_STAR, + [aux_sym_block_comment_token1] = aux_sym_block_comment_token1, + [aux_sym_block_comment_token2] = aux_sym_block_comment_token2, + [aux_sym_block_comment_token3] = aux_sym_block_comment_token3, + [anon_sym_STAR_SLASH] = anon_sym_STAR_SLASH, + [sym_string_literal] = sym_string_literal, + [sym_char_literal] = sym_char_literal, + [sym_integer_literal] = sym_integer_literal, + [sym_builtin] = sym_builtin, + [sym_builtin_type] = sym_builtin_type, + [sym_keyword] = sym_keyword, + [sym_identifier] = sym_identifier, + [sym_operator] = sym_operator, + [sym_punctuation] = sym_punctuation, + [sym_source_file] = sym_source_file, + [sym_block_comment] = sym_block_comment, + [aux_sym_source_file_repeat1] = aux_sym_source_file_repeat1, + [aux_sym_block_comment_repeat1] = aux_sym_block_comment_repeat1, +}; + +static const TSSymbolMetadata ts_symbol_metadata[] = { + [ts_builtin_sym_end] = { + .visible = false, + .named = true, + }, + [sym_line_comment] = { + .visible = true, + .named = true, + }, + [anon_sym_SLASH_STAR] = { + .visible = true, + .named = false, + }, + [aux_sym_block_comment_token1] = { + .visible = false, + .named = false, + }, + [aux_sym_block_comment_token2] = { + .visible = false, + .named = false, + }, + [aux_sym_block_comment_token3] = { + .visible = false, + .named = false, + }, + [anon_sym_STAR_SLASH] = { + .visible = true, + .named = false, + }, + [sym_string_literal] = { + .visible = true, + .named = true, + }, + [sym_char_literal] = { + .visible = true, + .named = true, + }, + [sym_integer_literal] = { + .visible = true, + .named = true, + }, + [sym_builtin] = { + .visible = true, + .named = true, + }, + [sym_builtin_type] = { + .visible = true, + .named = true, + }, + [sym_keyword] = { + .visible = true, + .named = true, + }, + [sym_identifier] = { + .visible = true, + .named = true, + }, + [sym_operator] = { + .visible = true, + .named = true, + }, + [sym_punctuation] = { + .visible = true, + .named = true, + }, + [sym_source_file] = { + .visible = true, + .named = true, + }, + [sym_block_comment] = { + .visible = true, + .named = true, + }, + [aux_sym_source_file_repeat1] = { + .visible = false, + .named = false, + }, + [aux_sym_block_comment_repeat1] = { + .visible = false, + .named = false, + }, +}; + +static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = { + [0] = {0}, +}; + +static const uint16_t ts_non_terminal_alias_map[] = { + 0, +}; + +static const TSStateId ts_primary_state_ids[STATE_COUNT] = { + [0] = 0, + [1] = 1, + [2] = 2, + [3] = 3, + [4] = 4, + [5] = 5, + [6] = 6, + [7] = 7, + [8] = 8, + [9] = 6, + [10] = 7, + [11] = 4, + [12] = 5, + [13] = 13, +}; + +static bool ts_lex(TSLexer *lexer, TSStateId state) { + START_LEXER(); + eof = lexer->eof(lexer); + switch (state) { + case 0: + if (eof) ADVANCE(21); + ADVANCE_MAP( + '!', 155, + '"', 1, + '%', 155, + '&', 155, + '\'', 6, + '*', 157, + '+', 157, + '-', 159, + '.', 161, + '/', 153, + '0', 31, + '<', 154, + '=', 158, + '>', 156, + '@', 20, + 'S', 69, + '^', 155, + 'a', 110, + 'b', 115, + 'c', 48, + 'd', 71, + 'e', 98, + 'f', 50, + 'i', 44, + 'l', 76, + 'm', 49, + 'n', 118, + 'o', 126, + 'p', 51, + 'r', 78, + 's', 68, + 't', 127, + 'u', 45, + 'v', 53, + 'w', 86, + '|', 155, + '?', 152, + '~', 152, + '(', 160, + ')', 160, + ',', 160, + ':', 160, + ';', 160, + '[', 160, + ']', 160, + '{', 160, + '}', 160, + ); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ' || + lookahead == 0x200b || + lookahead == 0x2060 || + lookahead == 0xfeff) SKIP(0); + if (('1' <= lookahead && lookahead <= '9')) ADVANCE(34); + if (('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('g' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 1: + if (lookahead == '"') ADVANCE(29); + if (lookahead == '\\') ADVANCE(7); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != '\r' && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(1); + END_STATE(); + case 2: + if (lookahead == '\'') ADVANCE(30); + END_STATE(); + case 3: + if (lookahead == '*') ADVANCE(23); + if (lookahead != 0 && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(27); + END_STATE(); + case 4: + if (lookahead == '*') ADVANCE(5); + if (lookahead == '/') ADVANCE(3); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ' || + lookahead == 0x200b || + lookahead == 0x2060 || + lookahead == 0xfeff) ADVANCE(24); + if (lookahead != 0 && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(25); + END_STATE(); + case 5: + if (lookahead == '/') ADVANCE(28); + if (lookahead != 0 && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(26); + END_STATE(); + case 6: + if (lookahead == '\\') ADVANCE(8); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != '\r' && + lookahead != '\'' && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(2); + END_STATE(); + case 7: + ADVANCE_MAP( + 'u', 17, + 'x', 13, + '"', 1, + '\'', 1, + '0', 1, + '\\', 1, + 'n', 1, + 'r', 1, + 't', 1, + ); + END_STATE(); + case 8: + ADVANCE_MAP( + 'u', 18, + 'x', 14, + '"', 2, + '\'', 2, + '0', 2, + '\\', 2, + 'n', 2, + 'r', 2, + 't', 2, + ); + END_STATE(); + case 9: + if (lookahead == '0' || + lookahead == '1' || + lookahead == '_') ADVANCE(32); + END_STATE(); + case 10: + if (('0' <= lookahead && lookahead <= '7') || + lookahead == '_') ADVANCE(33); + END_STATE(); + case 11: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(1); + END_STATE(); + case 12: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(2); + END_STATE(); + case 13: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(11); + END_STATE(); + case 14: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(12); + END_STATE(); + case 15: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(13); + END_STATE(); + case 16: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(14); + END_STATE(); + case 17: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(15); + END_STATE(); + case 18: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(16); + END_STATE(); + case 19: + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(35); + END_STATE(); + case 20: + if (('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(36); + END_STATE(); + case 21: + ACCEPT_TOKEN(ts_builtin_sym_end); + END_STATE(); + case 22: + ACCEPT_TOKEN(sym_line_comment); + if (lookahead != 0 && + lookahead != '\n' && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(22); + END_STATE(); + case 23: + ACCEPT_TOKEN(anon_sym_SLASH_STAR); + END_STATE(); + case 24: + ACCEPT_TOKEN(aux_sym_block_comment_token1); + if (('\t' <= lookahead && lookahead <= '\r') || + lookahead == ' ' || + lookahead == 0x200b || + lookahead == 0x2060 || + lookahead == 0xfeff) ADVANCE(24); + if (lookahead != 0 && + lookahead != '*' && + lookahead != '/' && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(25); + END_STATE(); + case 25: + ACCEPT_TOKEN(aux_sym_block_comment_token1); + if (lookahead != 0 && + lookahead != '*' && + lookahead != '/' && + lookahead != 0x17f && + lookahead != 0x212a) ADVANCE(25); + END_STATE(); + case 26: + ACCEPT_TOKEN(aux_sym_block_comment_token2); + END_STATE(); + case 27: + ACCEPT_TOKEN(aux_sym_block_comment_token3); + END_STATE(); + case 28: + ACCEPT_TOKEN(anon_sym_STAR_SLASH); + END_STATE(); + case 29: + ACCEPT_TOKEN(sym_string_literal); + END_STATE(); + case 30: + ACCEPT_TOKEN(sym_char_literal); + END_STATE(); + case 31: + ACCEPT_TOKEN(sym_integer_literal); + if (lookahead == 'B' || + lookahead == 'b') ADVANCE(9); + if (lookahead == 'O' || + lookahead == 'o') ADVANCE(10); + if (lookahead == 'X' || + lookahead == 'x') ADVANCE(19); + if (('0' <= lookahead && lookahead <= '9') || + lookahead == '_') ADVANCE(34); + END_STATE(); + case 32: + ACCEPT_TOKEN(sym_integer_literal); + if (lookahead == '0' || + lookahead == '1' || + lookahead == '_') ADVANCE(32); + END_STATE(); + case 33: + ACCEPT_TOKEN(sym_integer_literal); + if (('0' <= lookahead && lookahead <= '7') || + lookahead == '_') ADVANCE(33); + END_STATE(); + case 34: + ACCEPT_TOKEN(sym_integer_literal); + if (('0' <= lookahead && lookahead <= '9') || + lookahead == '_') ADVANCE(34); + END_STATE(); + case 35: + ACCEPT_TOKEN(sym_integer_literal); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'F') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'f')) ADVANCE(35); + END_STATE(); + case 36: + ACCEPT_TOKEN(sym_builtin); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(36); + END_STATE(); + case 37: + ACCEPT_TOKEN(sym_builtin_type); + END_STATE(); + case 38: + ACCEPT_TOKEN(sym_builtin_type); + if (lookahead == 'u') ADVANCE(63); + END_STATE(); + case 39: + ACCEPT_TOKEN(sym_keyword); + END_STATE(); + case 40: + ACCEPT_TOKEN(sym_keyword); + if (lookahead == '_') ADVANCE(135); + END_STATE(); + case 41: + ACCEPT_TOKEN(sym_keyword); + if (lookahead == 'e') ADVANCE(100); + END_STATE(); + case 42: + ACCEPT_TOKEN(sym_keyword); + if (lookahead == 'm') ADVANCE(39); + END_STATE(); + case 43: + ACCEPT_TOKEN(sym_keyword); + if (lookahead == 't') ADVANCE(80); + END_STATE(); + case 44: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == '1') ADVANCE(47); + if (lookahead == '3') ADVANCE(46); + if (lookahead == '8') ADVANCE(37); + if (lookahead == 'f') ADVANCE(39); + if (lookahead == 'm') ADVANCE(120); + if (lookahead == 'n') ADVANCE(43); + if (lookahead == 's') ADVANCE(87); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 45: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == '1') ADVANCE(47); + if (lookahead == '3') ADVANCE(46); + if (lookahead == '8') ADVANCE(37); + if (lookahead == 'n') ADVANCE(67); + if (lookahead == 's') ADVANCE(87); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 46: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == '2') ADVANCE(37); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 47: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == '6') ADVANCE(37); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 48: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(140); + if (lookahead == 'h') ADVANCE(54); + if (lookahead == 'o') ADVANCE(107); + if (lookahead == 'r') ADVANCE(93); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 49: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(140); + if (lookahead == 'u') ADVANCE(137); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 50: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(99); + if (lookahead == 'n') ADVANCE(39); + if (lookahead == 'o') ADVANCE(124); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 51: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(61); + if (lookahead == 'u') ADVANCE(59); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 52: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(96); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 53: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(124); + if (lookahead == 'o') ADVANCE(89); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 54: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(125); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 55: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(101); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 56: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(133); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 57: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(83); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 58: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'a') ADVANCE(141); + if (lookahead == 'r') ADVANCE(38); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 59: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'b') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 60: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'c') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 61: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'c') ADVANCE(97); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 62: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'c') ADVANCE(85); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 63: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'c') ADVANCE(137); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 64: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'c') ADVANCE(55); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 65: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'd') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 66: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'd') ADVANCE(37); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 67: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'd') ADVANCE(77); + if (lookahead == 'i') ADVANCE(137); + if (lookahead == 's') ADVANCE(57); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 68: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(103); + if (lookahead == 'h') ADVANCE(56); + if (lookahead == 't') ADVANCE(58); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 69: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(103); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 70: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(65); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 71: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(82); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 72: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 73: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(37); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 74: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(52); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 75: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(124); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 76: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(137); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 77: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(84); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 78: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(139); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 79: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(128); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 80: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'e') ADVANCE(132); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 81: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'f') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 82: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'f') ADVANCE(75); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 83: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'f') ADVANCE(72); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 84: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'f') ADVANCE(91); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 85: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'h') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 86: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'h') ADVANCE(95); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 87: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(150); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 88: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(112); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 89: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(66); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 90: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(109); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 91: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(113); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 92: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(60); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 93: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(143); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 94: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(64); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 95: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'i') ADVANCE(105); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 96: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'k') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 97: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'k') ADVANCE(70); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 98: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(136); + if (lookahead == 'n') ADVANCE(145); + if (lookahead == 'r') ADVANCE(129); + if (lookahead == 'x') ADVANCE(142); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 99: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(136); + if (lookahead == 'r') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 100: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(136); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 101: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 102: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(37); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 103: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(81); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 104: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(101); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 105: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'l') ADVANCE(72); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 106: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'm') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 107: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'm') ADVANCE(123); + if (lookahead == 'n') ADVANCE(134); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 108: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'm') ADVANCE(92); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 109: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'm') ADVANCE(72); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 110: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'n') ADVANCE(65); + if (lookahead == 's') ADVANCE(42); + if (lookahead == 't') ADVANCE(114); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 111: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'n') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 112: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'n') ADVANCE(147); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 113: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'n') ADVANCE(70); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 114: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'o') ADVANCE(108); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 115: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'o') ADVANCE(117); + if (lookahead == 'r') ADVANCE(74); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 116: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'o') ADVANCE(124); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 117: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'o') ADVANCE(102); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 118: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'o') ADVANCE(137); + if (lookahead == 'u') ADVANCE(104); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 119: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'o') ADVANCE(130); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 120: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'p') ADVANCE(119); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 121: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'p') ADVANCE(72); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 122: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'p') ADVANCE(138); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 123: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'p') ADVANCE(144); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 124: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 125: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(37); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 126: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(41); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 127: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(146); + if (lookahead == 'y') ADVANCE(121); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 128: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(111); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 129: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(116); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 130: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(137); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 131: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(149); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 132: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(131); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 133: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'r') ADVANCE(70); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 134: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 's') ADVANCE(137); + if (lookahead == 't') ADVANCE(88); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 135: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 's') ADVANCE(57); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 136: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 's') ADVANCE(72); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 137: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 138: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(40); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 139: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(148); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 140: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(62); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 141: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(92); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 142: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(79); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 143: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(94); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 144: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 't') ADVANCE(90); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 145: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'u') ADVANCE(106); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 146: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'u') ADVANCE(72); + if (lookahead == 'y') ADVANCE(39); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 147: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'u') ADVANCE(72); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 148: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'u') ADVANCE(128); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 149: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'u') ADVANCE(122); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 150: + ACCEPT_TOKEN(sym_identifier); + if (lookahead == 'z') ADVANCE(73); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'y')) ADVANCE(151); + END_STATE(); + case 151: + ACCEPT_TOKEN(sym_identifier); + if (('0' <= lookahead && lookahead <= '9') || + ('A' <= lookahead && lookahead <= 'Z') || + lookahead == '_' || + ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); + END_STATE(); + case 152: + ACCEPT_TOKEN(sym_operator); + END_STATE(); + case 153: + ACCEPT_TOKEN(sym_operator); + if (lookahead == '*') ADVANCE(23); + if (lookahead == '/') ADVANCE(22); + if (lookahead == '=') ADVANCE(152); + END_STATE(); + case 154: + ACCEPT_TOKEN(sym_operator); + if (lookahead == '<') ADVANCE(155); + if (lookahead == '=') ADVANCE(152); + END_STATE(); + case 155: + ACCEPT_TOKEN(sym_operator); + if (lookahead == '=') ADVANCE(152); + END_STATE(); + case 156: + ACCEPT_TOKEN(sym_operator); + if (lookahead == '=') ADVANCE(152); + if (lookahead == '>') ADVANCE(155); + END_STATE(); + case 157: + ACCEPT_TOKEN(sym_operator); + if (lookahead == '%' || + lookahead == '=') ADVANCE(152); + END_STATE(); + case 158: + ACCEPT_TOKEN(sym_operator); + if (lookahead == '=' || + lookahead == '>') ADVANCE(152); + END_STATE(); + case 159: + ACCEPT_TOKEN(sym_operator); + if (lookahead == '%' || + lookahead == '=' || + lookahead == '>') ADVANCE(152); + END_STATE(); + case 160: + ACCEPT_TOKEN(sym_punctuation); + END_STATE(); + case 161: + ACCEPT_TOKEN(sym_punctuation); + if (lookahead == '.') ADVANCE(152); + END_STATE(); + default: + return false; + } +} + +static const TSLexMode ts_lex_modes[STATE_COUNT] = { + [0] = {.lex_state = 0}, + [1] = {.lex_state = 0}, + [2] = {.lex_state = 0}, + [3] = {.lex_state = 0}, + [4] = {.lex_state = 0}, + [5] = {.lex_state = 0}, + [6] = {.lex_state = 4}, + [7] = {.lex_state = 4}, + [8] = {.lex_state = 4}, + [9] = {.lex_state = 4}, + [10] = {.lex_state = 4}, + [11] = {.lex_state = 4}, + [12] = {.lex_state = 4}, + [13] = {.lex_state = 0}, +}; + +static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { + [STATE(0)] = { + [ts_builtin_sym_end] = ACTIONS(1), + [sym_line_comment] = ACTIONS(1), + [anon_sym_SLASH_STAR] = ACTIONS(1), + [sym_string_literal] = ACTIONS(1), + [sym_char_literal] = ACTIONS(1), + [sym_integer_literal] = ACTIONS(1), + [sym_builtin] = ACTIONS(1), + [sym_builtin_type] = ACTIONS(1), + [sym_keyword] = ACTIONS(1), + [sym_identifier] = ACTIONS(1), + [sym_operator] = ACTIONS(1), + [sym_punctuation] = ACTIONS(1), + }, + [STATE(1)] = { + [sym_source_file] = STATE(13), + [sym_block_comment] = STATE(2), + [aux_sym_source_file_repeat1] = STATE(2), + [ts_builtin_sym_end] = ACTIONS(3), + [sym_line_comment] = ACTIONS(5), + [anon_sym_SLASH_STAR] = ACTIONS(7), + [sym_string_literal] = ACTIONS(5), + [sym_char_literal] = ACTIONS(5), + [sym_integer_literal] = ACTIONS(5), + [sym_builtin] = ACTIONS(5), + [sym_builtin_type] = ACTIONS(9), + [sym_keyword] = ACTIONS(5), + [sym_identifier] = ACTIONS(9), + [sym_operator] = ACTIONS(9), + [sym_punctuation] = ACTIONS(9), + }, + [STATE(2)] = { + [sym_block_comment] = STATE(3), + [aux_sym_source_file_repeat1] = STATE(3), + [ts_builtin_sym_end] = ACTIONS(11), + [sym_line_comment] = ACTIONS(13), + [anon_sym_SLASH_STAR] = ACTIONS(7), + [sym_string_literal] = ACTIONS(13), + [sym_char_literal] = ACTIONS(13), + [sym_integer_literal] = ACTIONS(13), + [sym_builtin] = ACTIONS(13), + [sym_builtin_type] = ACTIONS(15), + [sym_keyword] = ACTIONS(13), + [sym_identifier] = ACTIONS(15), + [sym_operator] = ACTIONS(15), + [sym_punctuation] = ACTIONS(15), + }, + [STATE(3)] = { + [sym_block_comment] = STATE(3), + [aux_sym_source_file_repeat1] = STATE(3), + [ts_builtin_sym_end] = ACTIONS(17), + [sym_line_comment] = ACTIONS(19), + [anon_sym_SLASH_STAR] = ACTIONS(22), + [sym_string_literal] = ACTIONS(19), + [sym_char_literal] = ACTIONS(19), + [sym_integer_literal] = ACTIONS(19), + [sym_builtin] = ACTIONS(19), + [sym_builtin_type] = ACTIONS(25), + [sym_keyword] = ACTIONS(19), + [sym_identifier] = ACTIONS(25), + [sym_operator] = ACTIONS(25), + [sym_punctuation] = ACTIONS(25), + }, + [STATE(4)] = { + [ts_builtin_sym_end] = ACTIONS(28), + [sym_line_comment] = ACTIONS(28), + [anon_sym_SLASH_STAR] = ACTIONS(28), + [sym_string_literal] = ACTIONS(28), + [sym_char_literal] = ACTIONS(28), + [sym_integer_literal] = ACTIONS(28), + [sym_builtin] = ACTIONS(28), + [sym_builtin_type] = ACTIONS(30), + [sym_keyword] = ACTIONS(28), + [sym_identifier] = ACTIONS(30), + [sym_operator] = ACTIONS(30), + [sym_punctuation] = ACTIONS(30), + }, + [STATE(5)] = { + [ts_builtin_sym_end] = ACTIONS(32), + [sym_line_comment] = ACTIONS(32), + [anon_sym_SLASH_STAR] = ACTIONS(32), + [sym_string_literal] = ACTIONS(32), + [sym_char_literal] = ACTIONS(32), + [sym_integer_literal] = ACTIONS(32), + [sym_builtin] = ACTIONS(32), + [sym_builtin_type] = ACTIONS(34), + [sym_keyword] = ACTIONS(32), + [sym_identifier] = ACTIONS(34), + [sym_operator] = ACTIONS(34), + [sym_punctuation] = ACTIONS(34), + }, +}; + +static const uint16_t ts_small_parse_table[] = { + [0] = 5, + ACTIONS(36), 1, + anon_sym_SLASH_STAR, + ACTIONS(38), 1, + aux_sym_block_comment_token1, + ACTIONS(42), 1, + anon_sym_STAR_SLASH, + ACTIONS(40), 2, + aux_sym_block_comment_token2, + aux_sym_block_comment_token3, + STATE(7), 2, + sym_block_comment, + aux_sym_block_comment_repeat1, + [18] = 5, + ACTIONS(36), 1, + anon_sym_SLASH_STAR, + ACTIONS(44), 1, + aux_sym_block_comment_token1, + ACTIONS(48), 1, + anon_sym_STAR_SLASH, + ACTIONS(46), 2, + aux_sym_block_comment_token2, + aux_sym_block_comment_token3, + STATE(8), 2, + sym_block_comment, + aux_sym_block_comment_repeat1, + [36] = 5, + ACTIONS(50), 1, + anon_sym_SLASH_STAR, + ACTIONS(53), 1, + aux_sym_block_comment_token1, + ACTIONS(59), 1, + anon_sym_STAR_SLASH, + ACTIONS(56), 2, + aux_sym_block_comment_token2, + aux_sym_block_comment_token3, + STATE(8), 2, + sym_block_comment, + aux_sym_block_comment_repeat1, + [54] = 5, + ACTIONS(36), 1, + anon_sym_SLASH_STAR, + ACTIONS(61), 1, + aux_sym_block_comment_token1, + ACTIONS(65), 1, + anon_sym_STAR_SLASH, + ACTIONS(63), 2, + aux_sym_block_comment_token2, + aux_sym_block_comment_token3, + STATE(10), 2, + sym_block_comment, + aux_sym_block_comment_repeat1, + [72] = 5, + ACTIONS(36), 1, + anon_sym_SLASH_STAR, + ACTIONS(44), 1, + aux_sym_block_comment_token1, + ACTIONS(67), 1, + anon_sym_STAR_SLASH, + ACTIONS(46), 2, + aux_sym_block_comment_token2, + aux_sym_block_comment_token3, + STATE(8), 2, + sym_block_comment, + aux_sym_block_comment_repeat1, + [90] = 2, + ACTIONS(28), 1, + aux_sym_block_comment_token1, + ACTIONS(30), 4, + anon_sym_SLASH_STAR, + aux_sym_block_comment_token2, + aux_sym_block_comment_token3, + anon_sym_STAR_SLASH, + [100] = 2, + ACTIONS(32), 1, + aux_sym_block_comment_token1, + ACTIONS(34), 4, + anon_sym_SLASH_STAR, + aux_sym_block_comment_token2, + aux_sym_block_comment_token3, + anon_sym_STAR_SLASH, + [110] = 1, + ACTIONS(69), 1, + ts_builtin_sym_end, +}; + +static const uint32_t ts_small_parse_table_map[] = { + [SMALL_STATE(6)] = 0, + [SMALL_STATE(7)] = 18, + [SMALL_STATE(8)] = 36, + [SMALL_STATE(9)] = 54, + [SMALL_STATE(10)] = 72, + [SMALL_STATE(11)] = 90, + [SMALL_STATE(12)] = 100, + [SMALL_STATE(13)] = 110, +}; + +static const TSParseActionEntry ts_parse_actions[] = { + [0] = {.entry = {.count = 0, .reusable = false}}, + [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(), + [3] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 0, 0, 0), + [5] = {.entry = {.count = 1, .reusable = true}}, SHIFT(2), + [7] = {.entry = {.count = 1, .reusable = true}}, SHIFT(6), + [9] = {.entry = {.count = 1, .reusable = false}}, SHIFT(2), + [11] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 1, 0, 0), + [13] = {.entry = {.count = 1, .reusable = true}}, SHIFT(3), + [15] = {.entry = {.count = 1, .reusable = false}}, SHIFT(3), + [17] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), + [19] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(3), + [22] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(6), + [25] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(3), + [28] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_block_comment, 2, 0, 0), + [30] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_block_comment, 2, 0, 0), + [32] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_block_comment, 3, 0, 0), + [34] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_block_comment, 3, 0, 0), + [36] = {.entry = {.count = 1, .reusable = false}}, SHIFT(9), + [38] = {.entry = {.count = 1, .reusable = true}}, SHIFT(7), + [40] = {.entry = {.count = 1, .reusable = false}}, SHIFT(7), + [42] = {.entry = {.count = 1, .reusable = false}}, SHIFT(4), + [44] = {.entry = {.count = 1, .reusable = true}}, SHIFT(8), + [46] = {.entry = {.count = 1, .reusable = false}}, SHIFT(8), + [48] = {.entry = {.count = 1, .reusable = false}}, SHIFT(5), + [50] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), SHIFT_REPEAT(9), + [53] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), SHIFT_REPEAT(8), + [56] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), SHIFT_REPEAT(8), + [59] = {.entry = {.count = 1, .reusable = false}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), + [61] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10), + [63] = {.entry = {.count = 1, .reusable = false}}, SHIFT(10), + [65] = {.entry = {.count = 1, .reusable = false}}, SHIFT(11), + [67] = {.entry = {.count = 1, .reusable = false}}, SHIFT(12), + [69] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(), +}; + +#ifdef __cplusplus +extern "C" { +#endif +#ifdef TREE_SITTER_HIDE_SYMBOLS +#define TS_PUBLIC +#elif defined(_WIN32) +#define TS_PUBLIC __declspec(dllexport) +#else +#define TS_PUBLIC __attribute__((visibility("default"))) +#endif + +TS_PUBLIC const TSLanguage *tree_sitter_ferro(void) { + static const TSLanguage language = { + .abi_version = LANGUAGE_VERSION, + .symbol_count = SYMBOL_COUNT, + .alias_count = ALIAS_COUNT, + .token_count = TOKEN_COUNT, + .external_token_count = EXTERNAL_TOKEN_COUNT, + .state_count = STATE_COUNT, + .large_state_count = LARGE_STATE_COUNT, + .production_id_count = PRODUCTION_ID_COUNT, + .field_count = FIELD_COUNT, + .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH, + .parse_table = &ts_parse_table[0][0], + .small_parse_table = ts_small_parse_table, + .small_parse_table_map = ts_small_parse_table_map, + .parse_actions = ts_parse_actions, + .symbol_names = ts_symbol_names, + .symbol_metadata = ts_symbol_metadata, + .public_symbol_map = ts_symbol_map, + .alias_map = ts_non_terminal_alias_map, + .alias_sequences = &ts_alias_sequences[0][0], + .lex_modes = (const void*)ts_lex_modes, + .lex_fn = ts_lex, + .primary_state_ids = ts_primary_state_ids, + }; + return &language; +} +#ifdef __cplusplus +} +#endif diff --git a/src/tree_sitter/parser.h b/src/tree_sitter/parser.h new file mode 100644 index 0000000..5f37997 --- /dev/null +++ b/src/tree_sitter/parser.h @@ -0,0 +1,55 @@ +#ifndef TREE_SITTER_PARSER_H_ +#define TREE_SITTER_PARSER_H_ +#ifdef __cplusplus +extern "C" { +#endif +#include +#include +#include +#define ts_builtin_sym_error ((TSSymbol)-1) +#define ts_builtin_sym_end 0 +#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 +#ifndef TREE_SITTER_API_H_ +typedef uint16_t TSStateId; +typedef uint16_t TSSymbol; +typedef uint16_t TSFieldId; +typedef struct TSLanguage TSLanguage; +typedef struct TSLanguageMetadata { uint8_t major_version; uint8_t minor_version; uint8_t patch_version; } TSLanguageMetadata; +#endif +typedef struct { TSFieldId field_id; uint8_t child_index; bool inherited; } TSFieldMapEntry; +typedef struct { uint16_t index; uint16_t length; } TSMapSlice; +typedef struct { bool visible; bool named; bool supertype; } TSSymbolMetadata; +typedef struct TSLexer TSLexer; +struct TSLexer { int32_t lookahead; TSSymbol result_symbol; void (*advance)(TSLexer *, bool); void (*mark_end)(TSLexer *); uint32_t (*get_column)(TSLexer *); bool (*is_at_included_range_start)(const TSLexer *); bool (*eof)(const TSLexer *); void (*log)(const TSLexer *, const char *, ...); }; +typedef enum { TSParseActionTypeShift, TSParseActionTypeReduce, TSParseActionTypeAccept, TSParseActionTypeRecover } TSParseActionType; +typedef union { struct { uint8_t type; TSStateId state; bool extra; bool repetition; } shift; struct { uint8_t type; uint8_t child_count; TSSymbol symbol; int16_t dynamic_precedence; uint16_t production_id; } reduce; uint8_t type; } TSParseAction; +typedef struct { uint16_t lex_state; uint16_t external_lex_state; } TSLexMode; +typedef struct { uint16_t lex_state; uint16_t external_lex_state; uint16_t reserved_word_set_id; } TSLexerMode; +typedef union { TSParseAction action; struct { uint8_t count; bool reusable; } entry; } TSParseActionEntry; +typedef struct { int32_t start; int32_t end; } TSCharacterRange; +struct TSLanguage { uint32_t abi_version; uint32_t symbol_count; uint32_t alias_count; uint32_t token_count; uint32_t external_token_count; uint32_t state_count; uint32_t large_state_count; uint32_t production_id_count; uint32_t field_count; uint16_t max_alias_sequence_length; const uint16_t *parse_table; const uint16_t *small_parse_table; const uint32_t *small_parse_table_map; const TSParseActionEntry *parse_actions; const char * const *symbol_names; const char * const *field_names; const TSMapSlice *field_map_slices; const TSFieldMapEntry *field_map_entries; const TSSymbolMetadata *symbol_metadata; const TSSymbol *public_symbol_map; const uint16_t *alias_map; const TSSymbol *alias_sequences; const TSLexerMode *lex_modes; bool (*lex_fn)(TSLexer *, TSStateId); bool (*keyword_lex_fn)(TSLexer *, TSStateId); TSSymbol keyword_capture_token; struct { const bool *states; const TSSymbol *symbol_map; void *(*create)(void); void (*destroy)(void *); bool (*scan)(void *, TSLexer *, const bool *); unsigned (*serialize)(void *, char *); void (*deserialize)(void *, const char *, unsigned); } external_scanner; const TSStateId *primary_state_ids; const char *name; const TSSymbol *reserved_words; uint16_t max_reserved_word_set_size; uint32_t supertype_count; const TSSymbol *supertype_symbols; const TSMapSlice *supertype_map_slices; const TSSymbol *supertype_map_entries; TSLanguageMetadata metadata; }; +static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { uint32_t index=0,size=len; while(size>1){uint32_t half=size/2,mid=index+half; const TSCharacterRange *r=&ranges[mid]; if(lookahead>=r->start&&lookahead<=r->end)return true; else if(lookahead>r->end)index=mid; size-=half;} const TSCharacterRange *r=&ranges[index]; return lookahead>=r->start&&lookahead<=r->end; } +#ifdef _MSC_VER +#define UNUSED __pragma(warning(suppress : 4101)) +#else +#define UNUSED __attribute__((unused)) +#endif +#define START_LEXER() bool result=false; bool skip=false; UNUSED bool eof=false; int32_t lookahead; goto start; next_state: lexer->advance(lexer,skip); start: skip=false; lookahead=lexer->lookahead; +#define ADVANCE(state_value) { state=state_value; goto next_state; } +#define ADVANCE_MAP(...) { static const uint16_t map[]={__VA_ARGS__}; for(uint32_t i=0;iresult_symbol=symbol_value; lexer->mark_end(lexer); +#define END_STATE() return result; +#define SMALL_STATE(id) ((id)-LARGE_STATE_COUNT) +#define STATE(id) id +#define ACTIONS(id) id +#define SHIFT(state_value) {{.shift={.type=TSParseActionTypeShift,.state=(state_value)}}} +#define SHIFT_REPEAT(state_value) {{.shift={.type=TSParseActionTypeShift,.state=(state_value),.repetition=true}}} +#define SHIFT_EXTRA() {{.shift={.type=TSParseActionTypeShift,.extra=true}}} +#define REDUCE(symbol_name,children,precedence,prod_id) {{.reduce={.type=TSParseActionTypeReduce,.symbol=symbol_name,.child_count=children,.dynamic_precedence=precedence,.production_id=prod_id},}} +#define RECOVER() {{.type=TSParseActionTypeRecover}} +#define ACCEPT_INPUT() {{.type=TSParseActionTypeAccept}} +#ifdef __cplusplus +} +#endif +#endif From d915a7e63cad589c40d6f79be3f0c409e679c2d8 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 18:54:23 +0900 Subject: [PATCH 039/184] chore: remove obsolete host test scripts --- fec/Makefile | 9 +--- fec/tests/run-cli-tests.sh | 26 ---------- fec/tests/run-tests.sh | 104 ------------------------------------- 3 files changed, 1 insertion(+), 138 deletions(-) delete mode 100644 fec/tests/run-cli-tests.sh delete mode 100644 fec/tests/run-tests.sh diff --git a/fec/Makefile b/fec/Makefile index 17c4f66..17626e4 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -4,7 +4,7 @@ CPPFLAGS ?= -Isrc SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/check.c src/emit_c.c src/driver.c OBJ = $(SRC:.c=.o) -.PHONY: all clean test test-cli dos-build +.PHONY: all clean dos-build all: fec fec: $(OBJ) @@ -13,13 +13,6 @@ fec: $(OBJ) src/%.o: src/%.c $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $< -test: fec - @./tests/run-tests.sh - @sh ./tests/run-cli-tests.sh - -test-cli: fec - @sh ./tests/run-cli-tests.sh - dos-build: @echo "Run build-dos.bat inside FreeDOS/Open Watcom." diff --git a/fec/tests/run-cli-tests.sh b/fec/tests/run-cli-tests.sh deleted file mode 100644 index ddecda3..0000000 --- a/fec/tests/run-cli-tests.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -set -eu -root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) -tmp=$(mktemp -d) -trap 'rm -rf "$tmp"' EXIT HUP INT TERM - -"$root"/fec --dump-tokens "$root"/tests/pass/basic.fe >"$tmp/tokens.out" -grep 'unit' "$tmp/tokens.out" >/dev/null -grep 'identifier' "$tmp/tokens.out" >/dev/null -grep 'eof' "$tmp/tokens.out" >/dev/null - -"$root"/fec --check "$root"/tests/m2/hello.fe >"$tmp/check.out" -if [ -s "$tmp/check.out" ]; then - echo "FAIL: --check produced stdout" - exit 1 -fi - -if "$root"/fec --check "$root"/tests/m2/bad-condition.fe >"$tmp/bad.out" 2>"$tmp/diag.out"; then - echo "FAIL: --check accepted invalid input" - exit 1 -fi -grep 'error:' "$tmp/diag.out" >/dev/null -grep '^ [0-9][0-9]* | ' "$tmp/diag.out" >/dev/null -grep ' | .*\^' "$tmp/diag.out" >/dev/null - -echo "CLI tests: token dump, check mode, and source diagnostics passed" diff --git a/fec/tests/run-tests.sh b/fec/tests/run-tests.sh deleted file mode 100644 index 9f159a7..0000000 --- a/fec/tests/run-tests.sh +++ /dev/null @@ -1,104 +0,0 @@ -#!/bin/sh -set -eu -root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) -ok=0 -for f in "$root"/std/*.fe; do - [ -f "$f" ] || continue - "$root"/fec --dump-ast "$f" >/dev/null || { echo "FAIL: $f"; exit 1; } - ok=$((ok+1)) -done -for f in "$root"/tests/pass/*.fe; do - [ -f "$f" ] || continue - "$root"/fec --dump-ast "$f" >/dev/null || { echo "FAIL: $f"; exit 1; } - ok=$((ok+1)) -done -for f in "$root"/tests/fail/*.fe; do - [ -f "$f" ] || continue - if "$root"/fec --dump-ast "$f" >/dev/null 2>/dev/null; then echo "FAIL (accepted): $f"; exit 1; fi - ok=$((ok+1)) -done -echo "M1 tests: $ok cases passed" - -m2tmp=$(mktemp -d) -trap 'rm -rf "$m2tmp"' EXIT HUP INT TERM -"$root"/fec --emit-c "$root"/tests/m2/hello.fe -o "$m2tmp/hello.c" -${CC:-cc} -std=c89 -pedantic "$m2tmp/hello.c" -o "$m2tmp/hello" -"$m2tmp/hello" -"$root"/fec --target=bits16 --emit-c "$root"/tests/m2/hello.fe -o "$m2tmp/hello16.c" -${CC:-cc} -std=c89 -pedantic "$m2tmp/hello16.c" -o "$m2tmp/hello16" -"$m2tmp/hello16" -"$root"/fec --emit-c "$root"/tests/m2/cast-while.fe -o "$m2tmp/cast.c" -${CC:-cc} -std=c89 -pedantic "$m2tmp/cast.c" -o "$m2tmp/cast" -"$m2tmp/cast" -"$root"/fec --emit-c "$root"/tests/m2/scopes.fe -o "$m2tmp/scopes.c" -${CC:-cc} -std=c89 -pedantic "$m2tmp/scopes.c" -o "$m2tmp/scopes" -"$m2tmp/scopes" -for f in bad-condition bad-cast bad-assign bad-unknown bad-arity bad-types bad-return bad-uninit bad-void; do - if "$root"/fec --emit-c "$root"/tests/m2/$f.fe -o "$m2tmp/$f.c" >/dev/null 2>/dev/null; then - echo "FAIL (accepted M2 semantic error): $f.fe" - exit 1 - fi -done -echo "M2 tests: integer control-flow smoke passed" - -m3tmp=$(mktemp -d) -trap 'rm -rf "$m2tmp" "$m3tmp"' EXIT HUP INT TERM -for f in struct enum array arrayctx str for nested char; do - "$root"/fec --target=bits32 --emit-c "$root"/tests/m3/$f.fe -o "$m3tmp/$f.c" - ${CC:-cc} -std=c89 -pedantic "$m3tmp/$f.c" -o "$m3tmp/$f" - "$m3tmp/$f" -done -"$root"/fec --target=bits32 --emit-c "$root"/tests/m3/bounds.fe -o "$m3tmp/bounds.c" -${CC:-cc} -std=c89 -pedantic "$m3tmp/bounds.c" -o "$m3tmp/bounds" -if "$m3tmp/bounds"; then - echo "FAIL (bounds trap did not fire): tests/m3/bounds.fe" - exit 1 -fi -for f in badfld badmat badarr badcycle badstr badchar badfield badindex; do - if "$root"/fec --target=bits32 --emit-c "$root"/tests/m3/$f.fe -o "$m3tmp/$f.c" >/dev/null 2>/dev/null; then - echo "FAIL (accepted M3 semantic error): $f.fe" - exit 1 - fi -done -echo "M3 tests: structs, enums, arrays, slices, str, match, and bounds passed" - -m4tmp=$(mktemp -d) -trap 'rm -rf "$m2tmp" "$m3tmp" "$m4tmp"' EXIT HUP INT TERM -for f in format; do - "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" - ${CC:-cc} -std=c89 -pedantic "$m4tmp/$f.c" -o "$m4tmp/$f" - "$m4tmp/$f" >"$m4tmp/$f.out" -done -for f in try-fprint; do - "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" - ${CC:-cc} -std=c89 -pedantic "$m4tmp/$f.c" -o "$m4tmp/$f" - "$m4tmp/$f" -done -"$root"/fec --target=bits32 --emit-c "$root"/tests/m4/prop.fe -o "$m4tmp/prop.c" -cp "$root"/tests/m4/proptest.c "$m4tmp/proptest.c" -${CC:-cc} -std=c89 -pedantic "$m4tmp/proptest.c" -o "$m4tmp/prop" -"$m4tmp/prop" -for f in bad-arity bad-verb bad-runtime bad-type bad-try bad-writer; do - if "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" >/dev/null 2>/dev/null; then - echo "FAIL (accepted M4 semantic error): $f.fe" - exit 1 - fi -done -for f in bad-many bad-open bad-cls; do - if "$root"/fec --target=bits32 --emit-c "$root"/tests/m4/$f.fe -o "$m4tmp/$f.c" >/dev/null 2>/dev/null; then - echo "FAIL (accepted M4 format-brace error): $f.fe" - exit 1 - fi -done -echo "M4 tests: formatting builtins passed" - -for f in defer owned; do - "$root"/fec --target=bits32 --emit-c "$root"/tests/m5/$f.fe -o "$m4tmp/m5-$f.c" -done -for f in bad-move bad-destroy; do - if "$root"/fec --target=bits32 --emit-c "$root"/tests/m5/$f.fe -o "$m4tmp/m5-$f.c" >/dev/null 2>/dev/null; then - echo "FAIL (accepted M5 ownership error): $f.fe" - exit 1 - fi -done -echo "M5 tests: owned move/defer checks passed" From 9986da6f12526e5698f05b1be109f85a30d47c6c Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 19:03:23 +0900 Subject: [PATCH 040/184] chore: isolate Zed clangd from Watcom headers --- .clangd | 6 ++++++ .zed/settings.json | 11 +++++++++++ 2 files changed, 17 insertions(+) create mode 100644 .clangd create mode 100644 .zed/settings.json diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..56a06c1 --- /dev/null +++ b/.clangd @@ -0,0 +1,6 @@ +# yaml-language-server: $schema=https://json.schemastore.org/clangd.json +CompileFlags: + Add: + - -xc + - -std=c89 + - -Ifec/src diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 0000000..2a3aa0d --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,11 @@ +{ + "lsp": { + "clangd": { + "binary": { + "env": { + "INCLUDE": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\BuildTools\\VC\\Tools\\MSVC\\14.29.30133\\include;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\ucrt;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\shared;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\um;C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.19041.0\\winrt" + } + } + } + } +} From f2e17d265fd4fc87287fbba911ce2018b48245fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:08:48 +0900 Subject: [PATCH 041/184] add M6-M9 specification tests (#3) --- fec/tests/m6/README.md | 11 +++++++++++ fec/tests/m6/badarg.fe | 10 ++++++++++ fec/tests/m6/baddefer.fe | 12 ++++++++++++ fec/tests/m6/badfld.fe | 6 ++++++ fec/tests/m6/badglob.fe | 9 +++++++++ fec/tests/m6/badgmut.fe | 9 +++++++++ fec/tests/m6/badinv.fe | 9 +++++++++ fec/tests/m6/badlocsl.fe | 7 +++++++ fec/tests/m6/badmove.fe | 10 ++++++++++ fec/tests/m6/badmut.fe | 10 ++++++++++ fec/tests/m6/badmut2.fe | 11 +++++++++++ fec/tests/m6/badptr.fe | 6 ++++++ fec/tests/m6/badret.fe | 7 +++++++ fec/tests/m6/badscop.fe | 12 ++++++++++++ fec/tests/m6/badself.fe | 10 ++++++++++ fec/tests/m6/badshwr.fe | 11 +++++++++++ fec/tests/m6/badslfld.fe | 6 ++++++ fec/tests/m6/badtwo.fe | 6 ++++++ fec/tests/m6/badup.fe | 10 ++++++++++ fec/tests/m6/okbranch.fe | 15 +++++++++++++++ fec/tests/m6/okdefer.fe | 13 +++++++++++++ fec/tests/m6/okglobcp.fe | 10 ++++++++++ fec/tests/m6/oklast.fe | 9 +++++++++ fec/tests/m6/okr8free.fe | 13 +++++++++++++ fec/tests/m6/okr8meth.fe | 17 +++++++++++++++++ fec/tests/m6/okr8stat.fe | 5 +++++ fec/tests/m6/okrebor.fe | 11 +++++++++++ fec/tests/m6/okshare.fe | 12 ++++++++++++ fec/tests/m6/okslreb.fe | 11 +++++++++++ fec/tests/m6/okstatic.fe | 11 +++++++++++ fec/tests/m6/oktemp.fe | 10 ++++++++++ fec/tests/m6/oktrim.fe | 5 +++++ fec/tests/m7/README.md | 8 ++++++++ fec/tests/m7/badcatch.fe | 17 +++++++++++++++++ fec/tests/m7/baddef.fe | 14 ++++++++++++++ fec/tests/m7/baddir.fe | 10 ++++++++++ fec/tests/m7/badetype.fe | 18 ++++++++++++++++++ fec/tests/m7/badoref.fe | 6 ++++++ fec/tests/m7/badorel.fe | 10 ++++++++++ fec/tests/m7/badproj.fe | 11 +++++++++++ fec/tests/m7/badqmark.fe | 7 +++++++ fec/tests/m7/badret.fe | 14 ++++++++++++++ fec/tests/m7/badsome.fe | 7 +++++++ fec/tests/m7/badtry.fe | 14 ++++++++++++++ fec/tests/m7/badzero.fe | 6 ++++++ fec/tests/m7/okcatch.fe | 16 ++++++++++++++++ fec/tests/m7/okcvoid.fe | 15 +++++++++++++++ fec/tests/m7/okdeflt.fe | 13 +++++++++++++ fec/tests/m7/okiflet.fe | 8 ++++++++ fec/tests/m7/okmatch.fe | 8 ++++++++ fec/tests/m7/okorelse.fe | 5 +++++ fec/tests/m7/okproj.fe | 9 +++++++++ fec/tests/m7/okrepl.fe | 11 +++++++++++ fec/tests/m7/oktrdef.fe | 14 ++++++++++++++ fec/tests/m7/oktry.fe | 14 ++++++++++++++ fec/tests/m8/README.md | 19 +++++++++++++++++++ fec/tests/m8/basic/main.fe | 6 ++++++ fec/tests/m8/basic/util.fe | 5 +++++ fec/tests/m8/cycle/a.fe | 4 ++++ fec/tests/m8/cycle/b.fe | 5 +++++ fec/tests/m8/errdet/alpha.fe | 5 +++++ fec/tests/m8/errdet/beta.fe | 5 +++++ fec/tests/m8/errdet/main.fe | 6 ++++++ fec/tests/m8/errnom/lib.fe | 9 +++++++++ fec/tests/m8/errnom/main.fe | 7 +++++++ fec/tests/m8/errsame/alpha.fe | 5 +++++ fec/tests/m8/errsame/beta.fe | 5 +++++ fec/tests/m8/errsame/main.fe | 6 ++++++ fec/tests/m8/missing/main.fe | 5 +++++ fec/tests/m8/privfld/data.fe | 5 +++++ fec/tests/m8/privfld/main.fe | 8 ++++++++ fec/tests/m8/privfn/main.fe | 7 +++++++ fec/tests/m8/privfn/util.fe | 5 +++++ fec/tests/m8/pubfld/data.fe | 5 +++++ fec/tests/m8/pubfld/main.fe | 7 +++++++ fec/tests/m8/unitbad/main.fe | 4 ++++ fec/tests/m9/README.md | 19 +++++++++++++++++++ fec/tests/m9/badarg.fe | 10 ++++++++++ fec/tests/m9/badarity.fe | 10 ++++++++++ fec/tests/m9/badbody.fe | 10 ++++++++++ fec/tests/m9/baddepth.fe | 10 ++++++++++ fec/tests/m9/badfew.fe | 10 ++++++++++ fec/tests/m9/badop.fe | 17 +++++++++++++++++ fec/tests/m9/badscope/lib.fe | 6 ++++++ fec/tests/m9/badscope/main.fe | 8 ++++++++ fec/tests/m9/badtype.fe | 6 ++++++ fec/tests/m9/defscope/lib.fe | 13 +++++++++++++ fec/tests/m9/defscope/main.fe | 6 ++++++ fec/tests/m9/okalias.fe | 11 +++++++++++ fec/tests/m9/okbox.fe | 18 ++++++++++++++++++ fec/tests/m9/okdedup.fe | 8 ++++++++ fec/tests/m9/okid.fe | 9 +++++++++ fec/tests/m9/okisint.fe | 13 +++++++++++++ fec/tests/m9/okmulti.fe | 11 +++++++++++ fec/tests/m9/oknested.fe | 15 +++++++++++++++ fec/tests/m9/okpair.fe | 15 +++++++++++++++ fec/tests/m9/oktypeeq.fe | 13 +++++++++++++ 97 files changed, 940 insertions(+) create mode 100644 fec/tests/m6/README.md create mode 100644 fec/tests/m6/badarg.fe create mode 100644 fec/tests/m6/baddefer.fe create mode 100644 fec/tests/m6/badfld.fe create mode 100644 fec/tests/m6/badglob.fe create mode 100644 fec/tests/m6/badgmut.fe create mode 100644 fec/tests/m6/badinv.fe create mode 100644 fec/tests/m6/badlocsl.fe create mode 100644 fec/tests/m6/badmove.fe create mode 100644 fec/tests/m6/badmut.fe create mode 100644 fec/tests/m6/badmut2.fe create mode 100644 fec/tests/m6/badptr.fe create mode 100644 fec/tests/m6/badret.fe create mode 100644 fec/tests/m6/badscop.fe create mode 100644 fec/tests/m6/badself.fe create mode 100644 fec/tests/m6/badshwr.fe create mode 100644 fec/tests/m6/badslfld.fe create mode 100644 fec/tests/m6/badtwo.fe create mode 100644 fec/tests/m6/badup.fe create mode 100644 fec/tests/m6/okbranch.fe create mode 100644 fec/tests/m6/okdefer.fe create mode 100644 fec/tests/m6/okglobcp.fe create mode 100644 fec/tests/m6/oklast.fe create mode 100644 fec/tests/m6/okr8free.fe create mode 100644 fec/tests/m6/okr8meth.fe create mode 100644 fec/tests/m6/okr8stat.fe create mode 100644 fec/tests/m6/okrebor.fe create mode 100644 fec/tests/m6/okshare.fe create mode 100644 fec/tests/m6/okslreb.fe create mode 100644 fec/tests/m6/okstatic.fe create mode 100644 fec/tests/m6/oktemp.fe create mode 100644 fec/tests/m6/oktrim.fe create mode 100644 fec/tests/m7/README.md create mode 100644 fec/tests/m7/badcatch.fe create mode 100644 fec/tests/m7/baddef.fe create mode 100644 fec/tests/m7/baddir.fe create mode 100644 fec/tests/m7/badetype.fe create mode 100644 fec/tests/m7/badoref.fe create mode 100644 fec/tests/m7/badorel.fe create mode 100644 fec/tests/m7/badproj.fe create mode 100644 fec/tests/m7/badqmark.fe create mode 100644 fec/tests/m7/badret.fe create mode 100644 fec/tests/m7/badsome.fe create mode 100644 fec/tests/m7/badtry.fe create mode 100644 fec/tests/m7/badzero.fe create mode 100644 fec/tests/m7/okcatch.fe create mode 100644 fec/tests/m7/okcvoid.fe create mode 100644 fec/tests/m7/okdeflt.fe create mode 100644 fec/tests/m7/okiflet.fe create mode 100644 fec/tests/m7/okmatch.fe create mode 100644 fec/tests/m7/okorelse.fe create mode 100644 fec/tests/m7/okproj.fe create mode 100644 fec/tests/m7/okrepl.fe create mode 100644 fec/tests/m7/oktrdef.fe create mode 100644 fec/tests/m7/oktry.fe create mode 100644 fec/tests/m8/README.md create mode 100644 fec/tests/m8/basic/main.fe create mode 100644 fec/tests/m8/basic/util.fe create mode 100644 fec/tests/m8/cycle/a.fe create mode 100644 fec/tests/m8/cycle/b.fe create mode 100644 fec/tests/m8/errdet/alpha.fe create mode 100644 fec/tests/m8/errdet/beta.fe create mode 100644 fec/tests/m8/errdet/main.fe create mode 100644 fec/tests/m8/errnom/lib.fe create mode 100644 fec/tests/m8/errnom/main.fe create mode 100644 fec/tests/m8/errsame/alpha.fe create mode 100644 fec/tests/m8/errsame/beta.fe create mode 100644 fec/tests/m8/errsame/main.fe create mode 100644 fec/tests/m8/missing/main.fe create mode 100644 fec/tests/m8/privfld/data.fe create mode 100644 fec/tests/m8/privfld/main.fe create mode 100644 fec/tests/m8/privfn/main.fe create mode 100644 fec/tests/m8/privfn/util.fe create mode 100644 fec/tests/m8/pubfld/data.fe create mode 100644 fec/tests/m8/pubfld/main.fe create mode 100644 fec/tests/m8/unitbad/main.fe create mode 100644 fec/tests/m9/README.md create mode 100644 fec/tests/m9/badarg.fe create mode 100644 fec/tests/m9/badarity.fe create mode 100644 fec/tests/m9/badbody.fe create mode 100644 fec/tests/m9/baddepth.fe create mode 100644 fec/tests/m9/badfew.fe create mode 100644 fec/tests/m9/badop.fe create mode 100644 fec/tests/m9/badscope/lib.fe create mode 100644 fec/tests/m9/badscope/main.fe create mode 100644 fec/tests/m9/badtype.fe create mode 100644 fec/tests/m9/defscope/lib.fe create mode 100644 fec/tests/m9/defscope/main.fe create mode 100644 fec/tests/m9/okalias.fe create mode 100644 fec/tests/m9/okbox.fe create mode 100644 fec/tests/m9/okdedup.fe create mode 100644 fec/tests/m9/okid.fe create mode 100644 fec/tests/m9/okisint.fe create mode 100644 fec/tests/m9/okmulti.fe create mode 100644 fec/tests/m9/oknested.fe create mode 100644 fec/tests/m9/okpair.fe create mode 100644 fec/tests/m9/oktypeeq.fe diff --git a/fec/tests/m6/README.md b/fec/tests/m6/README.md new file mode 100644 index 0000000..9e1e0ef --- /dev/null +++ b/fec/tests/m6/README.md @@ -0,0 +1,11 @@ +# M6 fixtures + +These fixtures pin the M6 ownership/borrow rules before implementation. + +- `ok*.fe` must compile with `--target=bits32 --emit-c`. +- `bad*.fe` must fail compilation; the first line follows the `// ERROR::` convention from SPEC §12. +- They are intentionally not wired into `TEST-DOS.BAT` while `master` is M5, so adding these fixtures does not make the current milestone red. +- When M6 starts, wire this directory into the DOS/QEMU gate without changing the expected result of any fixture. +- M6 also owns the general-global borrow restriction from R10 because AGENTS.md explicitly groups that change with the `own.c` state-machine work. + +Coverage: R4 storage restrictions, R5 scope, R6 shared/exclusive liveness and reborrows, R7 invalidation, R8 derived returns, defer lifetime extension, and global borrow restrictions. diff --git a/fec/tests/m6/badarg.fe b/fec/tests/m6/badarg.fe new file mode 100644 index 0000000..355fb9c --- /dev/null +++ b/fec/tests/m6/badarg.fe @@ -0,0 +1,10 @@ +// ERROR:8:self +unit badarg; + +struct Box { + value: i32, + + fn bad(self: &Self, other: &i32) -> &i32 { + return other; + } +} diff --git a/fec/tests/m6/baddefer.fe b/fec/tests/m6/baddefer.fe new file mode 100644 index 0000000..faf7839 --- /dev/null +++ b/fec/tests/m6/baddefer.fe @@ -0,0 +1,12 @@ +// ERROR:10:borrow +unit baddefer; + +fn read(r: &i32) -> i32 { return r.^; } + +fn bad() -> i32 { + var x: i32 = 0; + let r = &x; + defer { let y = read(r); } + x = 1; + return x; +} diff --git a/fec/tests/m6/badfld.fe b/fec/tests/m6/badfld.fe new file mode 100644 index 0000000..d4090ff --- /dev/null +++ b/fec/tests/m6/badfld.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit badfld; + +struct Bad { + value: &i32, +} diff --git a/fec/tests/m6/badglob.fe b/fec/tests/m6/badglob.fe new file mode 100644 index 0000000..41a107c --- /dev/null +++ b/fec/tests/m6/badglob.fe @@ -0,0 +1,9 @@ +// ERROR:7:global +unit badglob; + +var VALUE: i32 = 0; + +fn bad() -> i32 { + let r = &VALUE; + return r.^; +} diff --git a/fec/tests/m6/badgmut.fe b/fec/tests/m6/badgmut.fe new file mode 100644 index 0000000..38bd2f8 --- /dev/null +++ b/fec/tests/m6/badgmut.fe @@ -0,0 +1,9 @@ +// ERROR:7:global +unit badgmut; + +var VALUE: i32 = 0; + +fn bad() -> void { + let r = &mut VALUE; + r.^ = 1; +} diff --git a/fec/tests/m6/badinv.fe b/fec/tests/m6/badinv.fe new file mode 100644 index 0000000..88f483c --- /dev/null +++ b/fec/tests/m6/badinv.fe @@ -0,0 +1,9 @@ +// ERROR:6:borrow +unit badinv; + +fn bad(p: ^i32, q: ^i32) -> void { + let r = &p; + p = q; + let keep = r; + mem.destroy(p); +} diff --git a/fec/tests/m6/badlocsl.fe b/fec/tests/m6/badlocsl.fe new file mode 100644 index 0000000..bdc8eff --- /dev/null +++ b/fec/tests/m6/badlocsl.fe @@ -0,0 +1,7 @@ +// ERROR:6:reference +unit badlocsl; + +fn bad() -> []u8 { + let a: [2]u8 = [1 as u8, 2 as u8]; + return a[..]; +} diff --git a/fec/tests/m6/badmove.fe b/fec/tests/m6/badmove.fe new file mode 100644 index 0000000..e746062 --- /dev/null +++ b/fec/tests/m6/badmove.fe @@ -0,0 +1,10 @@ +// ERROR:8:borrow +unit badmove; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn bad(p: ^i32) -> void { + let r = &p; + take(p); + let q = r; +} diff --git a/fec/tests/m6/badmut.fe b/fec/tests/m6/badmut.fe new file mode 100644 index 0000000..90aab1f --- /dev/null +++ b/fec/tests/m6/badmut.fe @@ -0,0 +1,10 @@ +// ERROR:7:borrow +unit badmut; + +fn bad() -> i32 { + var x: i32 = 0; + let r = &mut x; + x = 1; + r.^ = 2; + return x; +} diff --git a/fec/tests/m6/badmut2.fe b/fec/tests/m6/badmut2.fe new file mode 100644 index 0000000..790e251 --- /dev/null +++ b/fec/tests/m6/badmut2.fe @@ -0,0 +1,11 @@ +// ERROR:7:borrow +unit badmut2; + +fn bad() -> i32 { + var x: i32 = 0; + let a = &mut x; + let b = &mut x; + a.^ = 1; + b.^ = 2; + return x; +} diff --git a/fec/tests/m6/badptr.fe b/fec/tests/m6/badptr.fe new file mode 100644 index 0000000..36e9490 --- /dev/null +++ b/fec/tests/m6/badptr.fe @@ -0,0 +1,6 @@ +// ERROR:4:reference +unit badptr; + +fn bad(p: *&i32) -> void { + return; +} diff --git a/fec/tests/m6/badret.fe b/fec/tests/m6/badret.fe new file mode 100644 index 0000000..5a9a777 --- /dev/null +++ b/fec/tests/m6/badret.fe @@ -0,0 +1,7 @@ +// ERROR:6:reference +unit badret; + +fn bad() -> &i32 { + let x: i32 = 1; + return &x; +} diff --git a/fec/tests/m6/badscop.fe b/fec/tests/m6/badscop.fe new file mode 100644 index 0000000..3bbeded --- /dev/null +++ b/fec/tests/m6/badscop.fe @@ -0,0 +1,12 @@ +// ERROR:9:reference +unit badscop; + +fn bad(cond: bool) -> i32 { + let outer: i32 = 0; + var r: &i32 = &outer; + if cond { + let inner: i32 = 3; + r = &inner; + } + return r.^; +} diff --git a/fec/tests/m6/badself.fe b/fec/tests/m6/badself.fe new file mode 100644 index 0000000..fe80c84 --- /dev/null +++ b/fec/tests/m6/badself.fe @@ -0,0 +1,10 @@ +// ERROR:8:self +unit badself; + +struct Box { + value: i32, + + fn bad(self: Self) -> &i32 { + return &self.value; + } +} diff --git a/fec/tests/m6/badshwr.fe b/fec/tests/m6/badshwr.fe new file mode 100644 index 0000000..015d14f --- /dev/null +++ b/fec/tests/m6/badshwr.fe @@ -0,0 +1,11 @@ +// ERROR:9:borrow +unit badshwr; + +fn read(r: &i32) -> i32 { return r.^; } + +fn bad() -> i32 { + var x: i32 = 0; + let r = &x; + x = 1; + return read(r); +} diff --git a/fec/tests/m6/badslfld.fe b/fec/tests/m6/badslfld.fe new file mode 100644 index 0000000..63b6c5a --- /dev/null +++ b/fec/tests/m6/badslfld.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit badslfld; + +struct Bad { + bytes: []u8, +} diff --git a/fec/tests/m6/badtwo.fe b/fec/tests/m6/badtwo.fe new file mode 100644 index 0000000..dcdfa0e --- /dev/null +++ b/fec/tests/m6/badtwo.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit badtwo; + +fn choose(a: &i32, b: &i32) -> &i32 { + return a; +} diff --git a/fec/tests/m6/badup.fe b/fec/tests/m6/badup.fe new file mode 100644 index 0000000..34f2b7e --- /dev/null +++ b/fec/tests/m6/badup.fe @@ -0,0 +1,10 @@ +// ERROR:8:mut +unit badup; + +struct Box { + value: i32, + + fn bad(self: &Self) -> &mut i32 { + return &mut self.value; + } +} diff --git a/fec/tests/m6/okbranch.fe b/fec/tests/m6/okbranch.fe new file mode 100644 index 0000000..f6964b8 --- /dev/null +++ b/fec/tests/m6/okbranch.fe @@ -0,0 +1,15 @@ +unit okbranch; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test(cond: bool) -> i32 { + var x: i32 = 3; + let r = &x; + if cond { + let a = read(r); + } else { + let b = read(r); + } + x += 1; + return x; +} diff --git a/fec/tests/m6/okdefer.fe b/fec/tests/m6/okdefer.fe new file mode 100644 index 0000000..1556b46 --- /dev/null +++ b/fec/tests/m6/okdefer.fe @@ -0,0 +1,13 @@ +unit okdefer; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 3; + if true { + let r = &x; + defer { let y = read(r); } + } + x += 1; + return x; +} diff --git a/fec/tests/m6/okglobcp.fe b/fec/tests/m6/okglobcp.fe new file mode 100644 index 0000000..e0c3e95 --- /dev/null +++ b/fec/tests/m6/okglobcp.fe @@ -0,0 +1,10 @@ +unit okglobcp; + +var VALUE: i32 = 7; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + let local = VALUE; + return read(&local); +} diff --git a/fec/tests/m6/oklast.fe b/fec/tests/m6/oklast.fe new file mode 100644 index 0000000..2e1e9f1 --- /dev/null +++ b/fec/tests/m6/oklast.fe @@ -0,0 +1,9 @@ +unit oklast; + +fn test() -> i32 { + var x: i32 = 0; + let r = &mut x; + r.^ = 1; + x += 1; + return x; +} diff --git a/fec/tests/m6/okr8free.fe b/fec/tests/m6/okr8free.fe new file mode 100644 index 0000000..0ae189e --- /dev/null +++ b/fec/tests/m6/okr8free.fe @@ -0,0 +1,13 @@ +unit okr8free; + +fn head(s: []u8) -> &u8 { + return &s[0]; +} + +fn test() -> u8 { + var a: [2]u8 = [1 as u8, 2 as u8]; + let r = head(a[..]); + let v = r.^; + a[0] = 7 as u8; + return v; +} diff --git a/fec/tests/m6/okr8meth.fe b/fec/tests/m6/okr8meth.fe new file mode 100644 index 0000000..8f49258 --- /dev/null +++ b/fec/tests/m6/okr8meth.fe @@ -0,0 +1,17 @@ +unit okr8meth; + +struct Box { + value: i32, + + pub fn get(self: &Self) -> &i32 { + return &self.value; + } +} + +fn test() -> i32 { + var b = Box{ value: 4 }; + let r = b.get(); + let v = r.^; + b.value = 5; + return v; +} diff --git a/fec/tests/m6/okr8stat.fe b/fec/tests/m6/okr8stat.fe new file mode 100644 index 0000000..5db67d4 --- /dev/null +++ b/fec/tests/m6/okr8stat.fe @@ -0,0 +1,5 @@ +unit okr8stat; + +fn name() -> str { + return "main"; +} diff --git a/fec/tests/m6/okrebor.fe b/fec/tests/m6/okrebor.fe new file mode 100644 index 0000000..b92ebf8 --- /dev/null +++ b/fec/tests/m6/okrebor.fe @@ -0,0 +1,11 @@ +unit okrebor; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 1; + let r = &mut x; + let v = read(r); + r.^ = v + 1; + return r.^; +} diff --git a/fec/tests/m6/okshare.fe b/fec/tests/m6/okshare.fe new file mode 100644 index 0000000..1740394 --- /dev/null +++ b/fec/tests/m6/okshare.fe @@ -0,0 +1,12 @@ +unit okshare; + +fn add(a: &i32, b: &i32) -> i32 { + return a.^ + b.^; +} + +fn test() -> i32 { + let x: i32 = 4; + let a = &x; + let b = &x; + return add(a, b); +} diff --git a/fec/tests/m6/okslreb.fe b/fec/tests/m6/okslreb.fe new file mode 100644 index 0000000..7b8a6a7 --- /dev/null +++ b/fec/tests/m6/okslreb.fe @@ -0,0 +1,11 @@ +unit okslreb; + +fn first(s: []u8) -> u8 { return s[0]; } + +fn test() -> u8 { + var a: [2]u8 = [1 as u8, 2 as u8]; + var s = a[..]; + let v = first(s); + s[0] = 9 as u8; + return v; +} diff --git a/fec/tests/m6/okstatic.fe b/fec/tests/m6/okstatic.fe new file mode 100644 index 0000000..ae2487e --- /dev/null +++ b/fec/tests/m6/okstatic.fe @@ -0,0 +1,11 @@ +unit okstatic; + +static VALUE: i32 = 7; + +fn get() -> &i32 { + return &VALUE; +} + +fn test() -> i32 { + return get().^; +} diff --git a/fec/tests/m6/oktemp.fe b/fec/tests/m6/oktemp.fe new file mode 100644 index 0000000..d88b1f7 --- /dev/null +++ b/fec/tests/m6/oktemp.fe @@ -0,0 +1,10 @@ +unit oktemp; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 1; + let v = read(&x); + x += 1; + return v + x; +} diff --git a/fec/tests/m6/oktrim.fe b/fec/tests/m6/oktrim.fe new file mode 100644 index 0000000..cf416ac --- /dev/null +++ b/fec/tests/m6/oktrim.fe @@ -0,0 +1,5 @@ +unit oktrim; + +fn trimmed(line: str) -> str { + return line.trim(); +} diff --git a/fec/tests/m7/README.md b/fec/tests/m7/README.md new file mode 100644 index 0000000..468c98b --- /dev/null +++ b/fec/tests/m7/README.md @@ -0,0 +1,8 @@ +# M7 fixtures + +M7 adds optionals and error unions on top of the M6 ownership model. + +`ok*.fe` must compile. `bad*.fe` must fail according to the first-line error marker. +The files are not wired into `TEST-DOS.BAT` until M7 work begins. + +Coverage: `?T`, `.?`, `Some`/`None` pattern-only destructuring, `mem.replace` extraction, `orelse`, nominal error unions, `try`, block/short `catch`, error code zero rejection, and R4/R7 interactions with optional references/owners. diff --git a/fec/tests/m7/badcatch.fe b/fec/tests/m7/badcatch.fe new file mode 100644 index 0000000..d6b2bdb --- /dev/null +++ b/fec/tests/m7/badcatch.fe @@ -0,0 +1,17 @@ +// ERROR:14:catch +unit badcatch; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn bad() -> i32 { + let v = leaf() catch |e| { + let ignored: i32 = 1; + }; + return v; +} diff --git a/fec/tests/m7/baddef.fe b/fec/tests/m7/baddef.fe new file mode 100644 index 0000000..2ff2b0f --- /dev/null +++ b/fec/tests/m7/baddef.fe @@ -0,0 +1,14 @@ +// ERROR:13:type +unit baddef; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn bad() -> i32 { + return leaf() catch false; +} diff --git a/fec/tests/m7/baddir.fe b/fec/tests/m7/baddir.fe new file mode 100644 index 0000000..46e0715 --- /dev/null +++ b/fec/tests/m7/baddir.fe @@ -0,0 +1,10 @@ +// ERROR:9:optional +unit baddir; + +struct Node { + value: i32, +} + +fn bad(p: ?^Node) -> i32 { + return p.^.value; +} diff --git a/fec/tests/m7/badetype.fe b/fec/tests/m7/badetype.fe new file mode 100644 index 0000000..af5bf39 --- /dev/null +++ b/fec/tests/m7/badetype.fe @@ -0,0 +1,18 @@ +// ERROR:17:error +unit badetype; + +error A { + Bad = 1, +} + +error B { + Bad = 1, +} + +fn leaf() -> A!i32 { + return A.Bad; +} + +fn bad() -> B!i32 { + return try leaf(); +} diff --git a/fec/tests/m7/badoref.fe b/fec/tests/m7/badoref.fe new file mode 100644 index 0000000..641206e --- /dev/null +++ b/fec/tests/m7/badoref.fe @@ -0,0 +1,6 @@ +// ERROR:5:reference +unit badoref; + +struct Bad { + value: ?&i32, +} diff --git a/fec/tests/m7/badorel.fe b/fec/tests/m7/badorel.fe new file mode 100644 index 0000000..3a79145 --- /dev/null +++ b/fec/tests/m7/badorel.fe @@ -0,0 +1,10 @@ +// ERROR:9:non-Copy +unit badorel; + +struct Node { + next: ?^Node, +} + +fn bad(p: ^Node, fallback: ^Node) -> ^Node { + return p.next orelse fallback; +} diff --git a/fec/tests/m7/badproj.fe b/fec/tests/m7/badproj.fe new file mode 100644 index 0000000..a7d987c --- /dev/null +++ b/fec/tests/m7/badproj.fe @@ -0,0 +1,11 @@ +// ERROR:9:mem.replace +unit badproj; + +struct Node { + next: ?^Node, +} + +fn bad(p: ^Node) -> void { + let q = p.next; + mem.destroy(p); +} diff --git a/fec/tests/m7/badqmark.fe b/fec/tests/m7/badqmark.fe new file mode 100644 index 0000000..36cdc7b --- /dev/null +++ b/fec/tests/m7/badqmark.fe @@ -0,0 +1,7 @@ +// ERROR:6:optional +unit badqmark; + +fn bad() -> i32 { + let x: i32 = 1; + return x.?; +} diff --git a/fec/tests/m7/badret.fe b/fec/tests/m7/badret.fe new file mode 100644 index 0000000..84842e2 --- /dev/null +++ b/fec/tests/m7/badret.fe @@ -0,0 +1,14 @@ +// ERROR:13:error +unit badret; + +error A { + Bad = 1, +} + +error B { + Bad = 1, +} + +fn bad() -> B!i32 { + return A.Bad; +} diff --git a/fec/tests/m7/badsome.fe b/fec/tests/m7/badsome.fe new file mode 100644 index 0000000..f35a461 --- /dev/null +++ b/fec/tests/m7/badsome.fe @@ -0,0 +1,7 @@ +// ERROR:5:Some +unit badsome; + +fn bad() -> i32 { + let x = Some(1); + return x; +} diff --git a/fec/tests/m7/badtry.fe b/fec/tests/m7/badtry.fe new file mode 100644 index 0000000..a41273e --- /dev/null +++ b/fec/tests/m7/badtry.fe @@ -0,0 +1,14 @@ +// ERROR:13:try +unit badtry; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn bad() -> i32 { + return try leaf(); +} diff --git a/fec/tests/m7/badzero.fe b/fec/tests/m7/badzero.fe new file mode 100644 index 0000000..da86afb --- /dev/null +++ b/fec/tests/m7/badzero.fe @@ -0,0 +1,6 @@ +// ERROR:5:0 +unit badzero; + +error E { + Zero = 0, +} diff --git a/fec/tests/m7/okcatch.fe b/fec/tests/m7/okcatch.fe new file mode 100644 index 0000000..557fd12 --- /dev/null +++ b/fec/tests/m7/okcatch.fe @@ -0,0 +1,16 @@ +unit okcatch; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn top() -> E!i32 { + let v = leaf() catch |e| { + return e; + }; + return v; +} diff --git a/fec/tests/m7/okcvoid.fe b/fec/tests/m7/okcvoid.fe new file mode 100644 index 0000000..4c1c2ab --- /dev/null +++ b/fec/tests/m7/okcvoid.fe @@ -0,0 +1,15 @@ +unit okcvoid; + +error E { + Bad = 1, +} + +fn leaf() -> E!void { + return E.Bad; +} + +fn top() -> void { + leaf() catch |e| { + return; + }; +} diff --git a/fec/tests/m7/okdeflt.fe b/fec/tests/m7/okdeflt.fe new file mode 100644 index 0000000..b62d66c --- /dev/null +++ b/fec/tests/m7/okdeflt.fe @@ -0,0 +1,13 @@ +unit okdeflt; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn top() -> i32 { + return leaf() catch 11; +} diff --git a/fec/tests/m7/okiflet.fe b/fec/tests/m7/okiflet.fe new file mode 100644 index 0000000..3192c4b --- /dev/null +++ b/fec/tests/m7/okiflet.fe @@ -0,0 +1,8 @@ +unit okiflet; + +fn value(p: ?i32) -> i32 { + if let Some(v) = p { + return v; + } + return 0; +} diff --git a/fec/tests/m7/okmatch.fe b/fec/tests/m7/okmatch.fe new file mode 100644 index 0000000..6335cff --- /dev/null +++ b/fec/tests/m7/okmatch.fe @@ -0,0 +1,8 @@ +unit okmatch; + +fn value(p: ?i32) -> i32 { + match p { + Some(v) => { return v; } + None => { return 0; } + } +} diff --git a/fec/tests/m7/okorelse.fe b/fec/tests/m7/okorelse.fe new file mode 100644 index 0000000..9ff537e --- /dev/null +++ b/fec/tests/m7/okorelse.fe @@ -0,0 +1,5 @@ +unit okorelse; + +fn value(p: ?i32) -> i32 { + return p orelse 9; +} diff --git a/fec/tests/m7/okproj.fe b/fec/tests/m7/okproj.fe new file mode 100644 index 0000000..f54768a --- /dev/null +++ b/fec/tests/m7/okproj.fe @@ -0,0 +1,9 @@ +unit okproj; + +struct Node { + value: i32, +} + +fn touch(p: ?&mut Node) -> void { + p.?.value = 7; +} diff --git a/fec/tests/m7/okrepl.fe b/fec/tests/m7/okrepl.fe new file mode 100644 index 0000000..2710132 --- /dev/null +++ b/fec/tests/m7/okrepl.fe @@ -0,0 +1,11 @@ +unit okrepl; + +struct Node { + value: i32, +} + +fn take(p: ?^Node) -> void { + var q: ?^Node = p; + let n = mem.replace(&mut q, null).?; + mem.destroy(n); +} diff --git a/fec/tests/m7/oktrdef.fe b/fec/tests/m7/oktrdef.fe new file mode 100644 index 0000000..1b0831c --- /dev/null +++ b/fec/tests/m7/oktrdef.fe @@ -0,0 +1,14 @@ +unit oktrdef; + +error E { + Bad = 1, +} + +fn leaf() -> E!i32 { + return E.Bad; +} + +fn top() -> E!i32 { + defer { let x: i32 = 1; } + return try leaf(); +} diff --git a/fec/tests/m7/oktry.fe b/fec/tests/m7/oktry.fe new file mode 100644 index 0000000..f0066db --- /dev/null +++ b/fec/tests/m7/oktry.fe @@ -0,0 +1,14 @@ +unit oktry; + +error E { + Bad = 1, +} + +fn leaf(ok: bool) -> E!i32 { + if ok { return 7; } + return E.Bad; +} + +fn top() -> E!i32 { + return try leaf(true); +} diff --git a/fec/tests/m8/README.md b/fec/tests/m8/README.md new file mode 100644 index 0000000..73e69e4 --- /dev/null +++ b/fec/tests/m8/README.md @@ -0,0 +1,19 @@ +# M8 fixtures + +M8 is the first multi-unit milestone, so cases live in subdirectories. Each case is compiled separately with that directory on the import path. + +Pass cases: +- `basic`: public function across units. +- `pubfld`: public type and public field across units. +- `errsame`: two units use the same anonymous `error.Name`; the driver must assign one deterministic `core.Error` code. +- `errdet`: the same anonymous error-name set appears in a different source/import order; generated `fe_errors.h` must be byte-identical to `errsame` modulo the intentionally different unit graph. + +Fail cases: +- `privfn`: private declaration access. +- `privfld`: private field access. +- `missing`: unresolved import. +- `cycle`: cyclic imports. +- `unitbad`: filename/unit-name mismatch. +- `errnom`: nominal error cannot flow into `core.Error` via `try`. + +The current M5 `TEST-DOS.BAT` is intentionally unchanged. M8 should add procedural checks for `.fei` creation/hash invalidation and deterministic `fe_errors.h` using these fixtures. diff --git a/fec/tests/m8/basic/main.fe b/fec/tests/m8/basic/main.fe new file mode 100644 index 0000000..98eae1e --- /dev/null +++ b/fec/tests/m8/basic/main.fe @@ -0,0 +1,6 @@ +unit main; +import util; + +fn main() -> i32 { + return util.answer(); +} diff --git a/fec/tests/m8/basic/util.fe b/fec/tests/m8/basic/util.fe new file mode 100644 index 0000000..8fb20a6 --- /dev/null +++ b/fec/tests/m8/basic/util.fe @@ -0,0 +1,5 @@ +unit util; + +pub fn answer() -> i32 { + return 42; +} diff --git a/fec/tests/m8/cycle/a.fe b/fec/tests/m8/cycle/a.fe new file mode 100644 index 0000000..03dbe1c --- /dev/null +++ b/fec/tests/m8/cycle/a.fe @@ -0,0 +1,4 @@ +unit a; +import b; + +pub fn a_value() -> i32 { return b.b_value(); } diff --git a/fec/tests/m8/cycle/b.fe b/fec/tests/m8/cycle/b.fe new file mode 100644 index 0000000..4ca3232 --- /dev/null +++ b/fec/tests/m8/cycle/b.fe @@ -0,0 +1,5 @@ +// ERROR:3:cycle +unit b; +import a; + +pub fn b_value() -> i32 { return 1; } diff --git a/fec/tests/m8/errdet/alpha.fe b/fec/tests/m8/errdet/alpha.fe new file mode 100644 index 0000000..1d8ec1d --- /dev/null +++ b/fec/tests/m8/errdet/alpha.fe @@ -0,0 +1,5 @@ +unit alpha; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/m8/errdet/beta.fe b/fec/tests/m8/errdet/beta.fe new file mode 100644 index 0000000..31e6b00 --- /dev/null +++ b/fec/tests/m8/errdet/beta.fe @@ -0,0 +1,5 @@ +unit beta; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/m8/errdet/main.fe b/fec/tests/m8/errdet/main.fe new file mode 100644 index 0000000..5386bb4 --- /dev/null +++ b/fec/tests/m8/errdet/main.fe @@ -0,0 +1,6 @@ +unit main; +import beta; +import alpha; + +fn one() -> !void { return beta.fail(); } +fn two() -> !void { return alpha.fail(); } diff --git a/fec/tests/m8/errnom/lib.fe b/fec/tests/m8/errnom/lib.fe new file mode 100644 index 0000000..58d1e15 --- /dev/null +++ b/fec/tests/m8/errnom/lib.fe @@ -0,0 +1,9 @@ +unit lib; + +pub error LibError { + Bad = 1, +} + +pub fn value() -> LibError!i32 { + return LibError.Bad; +} diff --git a/fec/tests/m8/errnom/main.fe b/fec/tests/m8/errnom/main.fe new file mode 100644 index 0000000..ee27d19 --- /dev/null +++ b/fec/tests/m8/errnom/main.fe @@ -0,0 +1,7 @@ +// ERROR:6:error +unit main; +import lib; + +fn bad() -> !i32 { + return try lib.value(); +} diff --git a/fec/tests/m8/errsame/alpha.fe b/fec/tests/m8/errsame/alpha.fe new file mode 100644 index 0000000..1d8ec1d --- /dev/null +++ b/fec/tests/m8/errsame/alpha.fe @@ -0,0 +1,5 @@ +unit alpha; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/m8/errsame/beta.fe b/fec/tests/m8/errsame/beta.fe new file mode 100644 index 0000000..31e6b00 --- /dev/null +++ b/fec/tests/m8/errsame/beta.fe @@ -0,0 +1,5 @@ +unit beta; + +pub fn fail() -> !void { + return error.Busy; +} diff --git a/fec/tests/m8/errsame/main.fe b/fec/tests/m8/errsame/main.fe new file mode 100644 index 0000000..0e46b1b --- /dev/null +++ b/fec/tests/m8/errsame/main.fe @@ -0,0 +1,6 @@ +unit main; +import alpha; +import beta; + +fn one() -> !void { return alpha.fail(); } +fn two() -> !void { return beta.fail(); } diff --git a/fec/tests/m8/missing/main.fe b/fec/tests/m8/missing/main.fe new file mode 100644 index 0000000..c95ad78 --- /dev/null +++ b/fec/tests/m8/missing/main.fe @@ -0,0 +1,5 @@ +// ERROR:3:import +unit main; +import absent; + +fn main() -> void {} diff --git a/fec/tests/m8/privfld/data.fe b/fec/tests/m8/privfld/data.fe new file mode 100644 index 0000000..cc36f31 --- /dev/null +++ b/fec/tests/m8/privfld/data.fe @@ -0,0 +1,5 @@ +unit data; + +pub struct Record { + value: i32, +} diff --git a/fec/tests/m8/privfld/main.fe b/fec/tests/m8/privfld/main.fe new file mode 100644 index 0000000..b870879 --- /dev/null +++ b/fec/tests/m8/privfld/main.fe @@ -0,0 +1,8 @@ +// ERROR:6:private +unit main; +import data; + +fn main() -> i32 { + let r = data.Record{ value: 7 }; + return r.value; +} diff --git a/fec/tests/m8/privfn/main.fe b/fec/tests/m8/privfn/main.fe new file mode 100644 index 0000000..d89c472 --- /dev/null +++ b/fec/tests/m8/privfn/main.fe @@ -0,0 +1,7 @@ +// ERROR:6:private +unit main; +import util; + +fn main() -> i32 { + return util.hidden(); +} diff --git a/fec/tests/m8/privfn/util.fe b/fec/tests/m8/privfn/util.fe new file mode 100644 index 0000000..ce93270 --- /dev/null +++ b/fec/tests/m8/privfn/util.fe @@ -0,0 +1,5 @@ +unit util; + +fn hidden() -> i32 { + return 1; +} diff --git a/fec/tests/m8/pubfld/data.fe b/fec/tests/m8/pubfld/data.fe new file mode 100644 index 0000000..7b04233 --- /dev/null +++ b/fec/tests/m8/pubfld/data.fe @@ -0,0 +1,5 @@ +unit data; + +pub struct Record { + pub value: i32, +} diff --git a/fec/tests/m8/pubfld/main.fe b/fec/tests/m8/pubfld/main.fe new file mode 100644 index 0000000..2af2ed1 --- /dev/null +++ b/fec/tests/m8/pubfld/main.fe @@ -0,0 +1,7 @@ +unit main; +import data; + +fn main() -> i32 { + let r = data.Record{ value: 7 }; + return r.value; +} diff --git a/fec/tests/m8/unitbad/main.fe b/fec/tests/m8/unitbad/main.fe new file mode 100644 index 0000000..3a2350f --- /dev/null +++ b/fec/tests/m8/unitbad/main.fe @@ -0,0 +1,4 @@ +// ERROR:2:unit +unit other; + +fn main() -> void {} diff --git a/fec/tests/m9/README.md b/fec/tests/m9/README.md new file mode 100644 index 0000000..ba80d35 --- /dev/null +++ b/fec/tests/m9/README.md @@ -0,0 +1,19 @@ +# M9 fixtures + +M9 adds comptime type parameters and monomorphization. + +`ok*.fe` must compile; `bad*.fe` must fail according to the first-line marker. +`defscope/` is a multi-unit definition-scope test and requires M8 imports. + +Coverage: +- generic functions and structs, +- multiple and duplicate instantiations, +- type aliases as comptime type values, +- `T == U` and `@is_int(T)` in comptime, +- instantiation-time operation errors, +- arity/type-argument errors, +- runtime `type` values forbidden, +- definition-unit name lookup, +- recursive instantiation depth limit. + +M9's DOS gate should additionally inspect generated `fe_generics.c`: `okdedup.fe` must emit one body for the repeated `(id, i32)` instantiation. diff --git a/fec/tests/m9/badarg.fe b/fec/tests/m9/badarg.fe new file mode 100644 index 0000000..18e207e --- /dev/null +++ b/fec/tests/m9/badarg.fe @@ -0,0 +1,10 @@ +// ERROR:9:comptime +unit badarg; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn bad() -> i32 { + return id(7, 8); +} diff --git a/fec/tests/m9/badarity.fe b/fec/tests/m9/badarity.fe new file mode 100644 index 0000000..6692d28 --- /dev/null +++ b/fec/tests/m9/badarity.fe @@ -0,0 +1,10 @@ +// ERROR:9:argument +unit badarity; + +struct Box(T) { + value: T, +} + +fn bad() -> void { + var x: Box(i32, u8); +} diff --git a/fec/tests/m9/badbody.fe b/fec/tests/m9/badbody.fe new file mode 100644 index 0000000..c909bd0 --- /dev/null +++ b/fec/tests/m9/badbody.fe @@ -0,0 +1,10 @@ +// ERROR:9:instantiation +unit badbody; + +fn add(comptime T: type, a: T, b: T) -> T { + return a + b; +} + +fn bad() -> bool { + return add(bool, true, false); +} diff --git a/fec/tests/m9/baddepth.fe b/fec/tests/m9/baddepth.fe new file mode 100644 index 0000000..a6d981b --- /dev/null +++ b/fec/tests/m9/baddepth.fe @@ -0,0 +1,10 @@ +// ERROR:9:depth +unit baddepth; + +struct Box(T) { + value: T, +} + +fn bad() -> void { + var x: Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(Box(i32))))))))))))))))))))))))))))))))); +} diff --git a/fec/tests/m9/badfew.fe b/fec/tests/m9/badfew.fe new file mode 100644 index 0000000..6b43ae3 --- /dev/null +++ b/fec/tests/m9/badfew.fe @@ -0,0 +1,10 @@ +// ERROR:9:generic +unit badfew; + +struct Box(T) { + value: T, +} + +fn bad() -> void { + var x: Box; +} diff --git a/fec/tests/m9/badop.fe b/fec/tests/m9/badop.fe new file mode 100644 index 0000000..872989d --- /dev/null +++ b/fec/tests/m9/badop.fe @@ -0,0 +1,17 @@ +// ERROR:16:instantiation +unit badop; + +struct Token { + value: i32, +} + +fn max(comptime T: type, a: T, b: T) -> T { + if a > b { return a; } + return b; +} + +fn bad() -> Token { + let a = Token{ value: 1 }; + let b = Token{ value: 2 }; + return max(Token, a, b); +} diff --git a/fec/tests/m9/badscope/lib.fe b/fec/tests/m9/badscope/lib.fe new file mode 100644 index 0000000..bba2221 --- /dev/null +++ b/fec/tests/m9/badscope/lib.fe @@ -0,0 +1,6 @@ +// ERROR:5:helper +unit lib; + +pub fn call(comptime T: type, v: T) -> T { + return helper(v); +} diff --git a/fec/tests/m9/badscope/main.fe b/fec/tests/m9/badscope/main.fe new file mode 100644 index 0000000..e7dd676 --- /dev/null +++ b/fec/tests/m9/badscope/main.fe @@ -0,0 +1,8 @@ +unit main; +import lib; + +fn helper(v: i32) -> i32 { return v + 10; } + +fn main() -> i32 { + return lib.call(i32, 1); +} diff --git a/fec/tests/m9/badtype.fe b/fec/tests/m9/badtype.fe new file mode 100644 index 0000000..b0b8325 --- /dev/null +++ b/fec/tests/m9/badtype.fe @@ -0,0 +1,6 @@ +// ERROR:5:type +unit badtype; + +fn bad() -> void { + let t = i32; +} diff --git a/fec/tests/m9/defscope/lib.fe b/fec/tests/m9/defscope/lib.fe new file mode 100644 index 0000000..4dcb06b --- /dev/null +++ b/fec/tests/m9/defscope/lib.fe @@ -0,0 +1,13 @@ +unit lib; + +fn bump_i32(v: i32) -> i32 { + return v + 1; +} + +pub fn bump(comptime T: type, v: T) -> T { + comptime if T == i32 { + return bump_i32(v); + } else { + return v; + } +} diff --git a/fec/tests/m9/defscope/main.fe b/fec/tests/m9/defscope/main.fe new file mode 100644 index 0000000..9c63d25 --- /dev/null +++ b/fec/tests/m9/defscope/main.fe @@ -0,0 +1,6 @@ +unit main; +import lib; + +fn main() -> i32 { + return lib.bump(i32, 4); +} diff --git a/fec/tests/m9/okalias.fe b/fec/tests/m9/okalias.fe new file mode 100644 index 0000000..a331e8d --- /dev/null +++ b/fec/tests/m9/okalias.fe @@ -0,0 +1,11 @@ +unit okalias; + +const Word = i32; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn test() -> Word { + return id(Word, 12); +} diff --git a/fec/tests/m9/okbox.fe b/fec/tests/m9/okbox.fe new file mode 100644 index 0000000..5a50c49 --- /dev/null +++ b/fec/tests/m9/okbox.fe @@ -0,0 +1,18 @@ +unit okbox; + +struct Box(T) { + value: T, + + pub fn new(v: T) -> Self { + return Self{ value: v }; + } + + pub fn get(self: &Self) -> &T { + return &self.value; + } +} + +fn test() -> i32 { + var b: Box(i32) = Box(i32).new(7); + return b.get().^; +} diff --git a/fec/tests/m9/okdedup.fe b/fec/tests/m9/okdedup.fe new file mode 100644 index 0000000..f243e91 --- /dev/null +++ b/fec/tests/m9/okdedup.fe @@ -0,0 +1,8 @@ +unit okdedup; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn a() -> i32 { return id(i32, 1); } +fn b() -> i32 { return id(i32, 2); } diff --git a/fec/tests/m9/okid.fe b/fec/tests/m9/okid.fe new file mode 100644 index 0000000..972f6e2 --- /dev/null +++ b/fec/tests/m9/okid.fe @@ -0,0 +1,9 @@ +unit okid; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn test() -> i32 { + return id(i32, 7); +} diff --git a/fec/tests/m9/okisint.fe b/fec/tests/m9/okisint.fe new file mode 100644 index 0000000..fba420c --- /dev/null +++ b/fec/tests/m9/okisint.fe @@ -0,0 +1,13 @@ +unit okisint; + +fn is_integer(comptime T: type) -> bool { + comptime if @is_int(T) { + return true; + } else { + return false; + } +} + +fn test() -> bool { + return is_integer(u16); +} diff --git a/fec/tests/m9/okmulti.fe b/fec/tests/m9/okmulti.fe new file mode 100644 index 0000000..cc20def --- /dev/null +++ b/fec/tests/m9/okmulti.fe @@ -0,0 +1,11 @@ +unit okmulti; + +fn id(comptime T: type, v: T) -> T { + return v; +} + +fn test() -> i32 { + let a: i32 = id(i32, 7); + let b: u8 = id(u8, 9 as u8); + return a + (b as i32); +} diff --git a/fec/tests/m9/oknested.fe b/fec/tests/m9/oknested.fe new file mode 100644 index 0000000..912786e --- /dev/null +++ b/fec/tests/m9/oknested.fe @@ -0,0 +1,15 @@ +unit oknested; + +struct Box(T) { + value: T, + + pub fn new(v: T) -> Self { + return Self{ value: v }; + } +} + +fn test() -> i32 { + let inner: Box(i32) = Box(i32).new(3); + let outer: Box(Box(i32)) = Box(Box(i32)).new(inner); + return outer.value.value; +} diff --git a/fec/tests/m9/okpair.fe b/fec/tests/m9/okpair.fe new file mode 100644 index 0000000..40af14e --- /dev/null +++ b/fec/tests/m9/okpair.fe @@ -0,0 +1,15 @@ +unit okpair; + +struct Pair(A, B) { + first: A, + second: B, + + pub fn new(a: A, b: B) -> Self { + return Self{ first: a, second: b }; + } +} + +fn test() -> i32 { + let p: Pair(i32, u8) = Pair(i32, u8).new(4, 5 as u8); + return p.first + (p.second as i32); +} diff --git a/fec/tests/m9/oktypeeq.fe b/fec/tests/m9/oktypeeq.fe new file mode 100644 index 0000000..49f7b9c --- /dev/null +++ b/fec/tests/m9/oktypeeq.fe @@ -0,0 +1,13 @@ +unit oktypeeq; + +fn kind(comptime T: type) -> i32 { + comptime if T == i32 { + return 1; + } else { + return 0; + } +} + +fn test() -> i32 { + return kind(i32); +} From 383c97581fb0516a16dc3e84124e1553da21a0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:21:40 +0900 Subject: [PATCH 042/184] noop --- THIS_SHOULD_NOT_EXIST.tmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 THIS_SHOULD_NOT_EXIST.tmp diff --git a/THIS_SHOULD_NOT_EXIST.tmp b/THIS_SHOULD_NOT_EXIST.tmp new file mode 100644 index 0000000..e69de29 From 0f67bf3780ddb2c2fdf78c4c17827279f5e44b2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:21:49 +0900 Subject: [PATCH 043/184] revert accidental temp file --- THIS_SHOULD_NOT_EXIST.tmp | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 THIS_SHOULD_NOT_EXIST.tmp diff --git a/THIS_SHOULD_NOT_EXIST.tmp b/THIS_SHOULD_NOT_EXIST.tmp deleted file mode 100644 index e69de29..0000000 From 7fd575859a6e75287c1261071ff8ca44319c18c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:44:51 +0900 Subject: [PATCH 044/184] extract M5 ownership state helpers --- fec/src/own.h | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 fec/src/own.h diff --git a/fec/src/own.h b/fec/src/own.h new file mode 100644 index 0000000..eb0ff65 --- /dev/null +++ b/fec/src/own.h @@ -0,0 +1,24 @@ +#ifndef FE_OWN_H +#define FE_OWN_H + +#include "types.h" +#include "diag.h" + +#define FE_OWN_NODE_CONSUMED 0x100U +#define FE_OWN_NODE_DEFER_CAPTURE 0x200U + +enum FeOwnMoveState { + FE_OWN_AVAILABLE = 0, + FE_OWN_MOVED = 1, + FE_OWN_MAYBE_MOVED = 2 +}; + +int fe_own_is_copy_type(FeType *type); +void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, + FeNode *expr, FeType *type, int in_defer); +void fe_own_check_use(FeDiags *diags, int state, FeLoc loc); +int fe_own_merge_move(int left, int right); +int fe_own_loop_entry(int before, int after); +int fe_own_loop_exit(int state, int after); + +#endif From 89cd035011edc22654a01d452280533d6995a45f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:45:02 +0900 Subject: [PATCH 045/184] extract M5 ownership state helpers --- fec/src/own.c | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 fec/src/own.c diff --git a/fec/src/own.c b/fec/src/own.c new file mode 100644 index 0000000..0d90817 --- /dev/null +++ b/fec/src/own.c @@ -0,0 +1,80 @@ +#include "own.h" + +int fe_own_is_copy_type(FeType *type) +{ + unsigned i; + if (!type) return 1; + if (type->kind == FE_TYPE_OWNED) return 0; + if (type->kind == FE_TYPE_REF || type->kind == FE_TYPE_SLICE) + return !type->ref_mut; + if (type->kind == FE_TYPE_ARRAY) + return fe_own_is_copy_type(type->elem); + if (type->kind == FE_TYPE_STRUCT) { + if (type->has_drop) return 0; + for (i = 0; i < type->field_count; ++i) + if (!fe_own_is_copy_type(type->fields[i].type)) return 0; + } + if (type->kind == FE_TYPE_ENUM) { + for (i = 0; i < type->variant_count; ++i) { + unsigned j; + for (j = 0; j < type->variants[i].field_count; ++j) + if (!fe_own_is_copy_type(type->variants[i].fields[j].type)) + return 0; + } + } + return 1; +} + +void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, + FeNode *expr, FeType *type, int in_defer) +{ + if (!expr || !type || fe_own_is_copy_type(type)) return; + if (expr->kind == FE_N_INDEX && type->kind == FE_TYPE_SLICE && + (expr->c || !expr->b)) + return; + if (expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) { + fe_diag_error(diags, expr->loc, + "cannot move a non-Copy value out of a projection; use mem.replace"); + return; + } + if (expr->kind != FE_N_IDENT || !state) return; + + if (in_defer) { + if (decl) decl->flags |= FE_OWN_NODE_DEFER_CAPTURE; + return; + } + + *state = FE_OWN_MOVED; + /* The consuming use owns the live-flag transition. The declaration must + stay live on control-flow paths where the move did not execute. */ + expr->flags |= FE_OWN_NODE_CONSUMED; +} + +void fe_own_check_use(FeDiags *diags, int state, FeLoc loc) +{ + if (state == FE_OWN_MOVED) + fe_diag_error(diags, loc, "use of moved value"); + else if (state == FE_OWN_MAYBE_MOVED) + fe_diag_error(diags, loc, "use of possibly moved value"); +} + +int fe_own_merge_move(int left, int right) +{ + if (left == FE_OWN_MOVED && right == FE_OWN_MOVED) + return FE_OWN_MOVED; + if (left != FE_OWN_AVAILABLE || right != FE_OWN_AVAILABLE) + return FE_OWN_MAYBE_MOVED; + return FE_OWN_AVAILABLE; +} + +int fe_own_loop_entry(int before, int after) +{ + if (before == after) return before; + return FE_OWN_MAYBE_MOVED; +} + +int fe_own_loop_exit(int state, int after) +{ + if (after != FE_OWN_AVAILABLE) return FE_OWN_MAYBE_MOVED; + return state; +} From accc7dd94ab2d5f6252bd87ead1077a9f55ffd75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:45:09 +0900 Subject: [PATCH 046/184] wire ownership helper into compiler build --- fec/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/Makefile b/fec/Makefile index 17626e4..ae23bb3 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -std=c89 CPPFLAGS ?= -Isrc -SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/check.c src/emit_c.c src/driver.c +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/own.c src/check.c src/emit_c.c src/driver.c OBJ = $(SRC:.c=.o) .PHONY: all clean dos-build From 5c88872ef05e2ff813906b11037455a6e6b032f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:45:20 +0900 Subject: [PATCH 047/184] wire ownership helper into DOS build --- fec/build-dos.bat | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fec/build-dos.bat b/fec/build-dos.bat index 38300c2..fbb7e5c 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -26,6 +26,8 @@ wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=parser.obj src\parser.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=types.obj src\types.c if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=own.obj src\own.c +if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check.c if errorlevel 1 goto build_fail rem Use an unambiguous short object name for the emit_c source. From db51611225c12d79341eef872a0ce801921670a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:47:19 +0900 Subject: [PATCH 048/184] route M5 ownership state through own.c --- fec/src/check.c | 67 +++++++++++-------------------------------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index ad5abb3..88d6588 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -1,4 +1,5 @@ #include "check.h" +#include "own.h" #include #include @@ -49,50 +50,15 @@ static int known(FeType *t) return t && t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ERROR; } -static int is_copy_type(FeType *t) -{ - unsigned i; - if (!t) return 1; - if (t->kind==FE_TYPE_OWNED) return 0; - if (t->kind==FE_TYPE_REF || t->kind==FE_TYPE_SLICE) return !t->ref_mut; - if (t->kind==FE_TYPE_ARRAY) return is_copy_type(t->elem); - if (t->kind==FE_TYPE_STRUCT) { - if (t->has_drop) return 0; - for (i=0;ifield_count;i++) if (!is_copy_type(t->fields[i].type)) return 0; - } - if (t->kind==FE_TYPE_ENUM) - for (i=0;ivariant_count;i++) { - unsigned j; - for (j=0;jvariants[i].field_count;j++) - if (!is_copy_type(t->variants[i].fields[j].type)) return 0; - } - return 1; -} - static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) { - FeSym *sym; - if (!n || !t || is_copy_type(t)) return; - if(n->kind==FE_N_INDEX && t->kind==FE_TYPE_SLICE && - (n->c || !n->b)) return; - if(n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX) { - err(s->c,n->loc, - "cannot move a non-Copy value out of a projection; use mem.replace"); - return; - } - if(n->kind!=FE_N_IDENT) return; - sym=find_symbol(s->scope,n->text ? n->text : ""); - if (sym) { - if (s->defer_depth) { - if (sym->decl) sym->decl->flags |= 0x200U; - } else { - sym->moved=1; - /* Mark this consuming expression, not the declaration. Branches - may move conditionally; the declaration's runtime live flag - must remain available to guard cleanup on the other path. */ - n->flags |= 0x100U; - } - } + FeSym *sym=0; + if (n && n->kind==FE_N_IDENT) + sym=find_symbol(s->scope,n->text ? n->text : ""); + fe_own_mark_consumed(s->c->diags, + sym ? &sym->moved : 0, + sym ? sym->decl : 0, + n,t,s->defer_depth != 0); } static int compatible(FeType *want, FeType *got, FeNode *value) @@ -244,7 +210,7 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, sym->fn = fn; sym->mutable = mutable; sym->initialized = initialized; - sym->moved = 0; + sym->moved = FE_OWN_AVAILABLE; sym->decl = decl; if (decl) { decl->cname = cname; @@ -329,9 +295,8 @@ static void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, { unsigned i; for (i=0; imoved = left[i].moved==1 && right[i].moved==1 ? 1 : - (left[i].moved || right[i].moved ? 2 : 0); - base[i].sym->initialized = left[i].initialized && right[i].initialized; + base[i].sym->moved=fe_own_merge_move(left[i].moved,right[i].moved); + base[i].sym->initialized=left[i].initialized && right[i].initialized; } } @@ -579,8 +544,7 @@ static FeType *check_identifier(FeCheckerState *s, FeNode *n, int read) } n->cname = sym->cname; n->sem_type = sym->type; - if (sym->moved == 1) err(s->c,n->loc,"use of moved value"); - else if (sym->moved == 2) err(s->c,n->loc,"use of possibly moved value"); + fe_own_check_use(s->c->diags,sym->moved,n->loc); if (read && !sym->initialized && !sym->fn) err(s->c, n->loc, "use of uninitialized variable"); return sym->type; @@ -1007,8 +971,7 @@ static void check_match(FeCheckerState *s, FeNode *n) have_merged=1; } else { for(i=0;iscope,body,flow_count); for (i=0;iloop_depth) --s->loop_depth; flow_capture(s->scope,body,flow_count); for(i=0;i Date: Sun, 16 Aug 2026 19:48:19 +0900 Subject: [PATCH 049/184] freeze M6-M9 semantics and unit model --- SPEC.AUDIT.md | 292 ++++++++++++++++++++++++++++++++++++ SPEC.md | 408 +++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 644 insertions(+), 56 deletions(-) diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index 09f4101..cb1c787 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -265,3 +265,295 @@ M6(borrow checker) 착수 전에 확정해야 하는 소유권·참조 규칙 - M7: try nominal error 일치와 일반 catch를 구현한다. - M8/M9: `fe_errors.h`, dependency hash, `fe_generics.c`를 구현한다. - M10: volatile/barrier, interrupt-safe 허용 목록, on_exit 복원을 검증한다. + +## 2026-08-16 — v0.1.8 + +M6~M9 구현 전에 함수-local 소유권 분석, optional/error 의미, 계층형 unit/import, +`.fei` cache와 제네릭 모노모피제이션을 동결했다. compiler A/B가 같은 작은 상태 기계를 +구현하고 DOS와 host에서 같은 source graph를 선택하며 M12 fixpoint에서 byte-identical +출력을 만들 수 있는지가 공통 판단 기준이다. + +### Deterministic expression evaluation order + +- 문제: C는 일반 호출 인자와 많은 operand의 평가 순서를 보장하지 않는다. Ferro가 이를 + 그대로 상속하면 side effect뿐 아니라 move, borrow, `try`, defer/drop cleanup 결과가 C + compiler와 최적화에 따라 달라진다. +- 결정: Ferro 일반 표현식은 left-to-right다. 호출은 callee 먼저, 이어서 source 순서의 + 인자, 이항식은 왼쪽 operand 먼저다. `and`/`or`, `orelse`, `catch`는 필요한 우변만 + 평가하는 lazy 연산이다. +- 근거: source만으로 동작과 cleanup 순서를 예측할 수 있고 compiler A/B의 lower 결과가 + 동일해진다. 함수-local 분석 원칙도 그대로 유지한다. +- 기각한 대안: target C의 평가 순서에 맡기기. host compiler와 build option에 따라 의미가 + 변해 M12 결정성을 깨므로 기각했다. +- 구현 영향: lower는 C에서 순서가 보장되지 않는 식을 ordered temporary statement로 + 분해하고 own.c도 같은 순서로 place effect를 처리한다. + +### M6 root-granularity borrow tracking + +- 문제: field/index별 독립 대여를 허용하려면 projection overlap, 동적 index 동등성, + union/alias까지 다루는 별도 alias analysis가 필요하다. +- 결정: v0.1 대여 상태는 root local/parameter 단위다. `&mut p.a`는 `p` 전체를 잠그고 + `xs[0]`과 `xs[1]`도 같은 root의 충돌 대여다. projection은 root를 찾는 데만 쓴다. +- 근거: R1~R8을 함수 하나의 작은 상태 기계로 검사할 수 있어 compiler A와 M11의 B가 + 단순해진다. 보수적 거부일 뿐 memory safety나 표현 결정성은 약화하지 않는다. +- 기각한 대안: field-sensitive/index-sensitive borrow checking. 편의는 늘지만 compiler A의 + 구현량과 진단 상태가 크게 증가하고 동적 index에는 결국 보수성이 남아 기각했다. +- 구현 영향: own.c의 borrow key는 projection이 아니라 root symbol이다. M6에는 disjoint + field/index도 충돌하는 pass/fail 경계를 고정한다. + +### M6 reborrow/coercion 제한 + +- 문제: `&mut → &`와 `[]mut → []`를 일반 암묵 변환으로 허용하면 새 shared borrow의 + 수명과 원래 exclusive borrow의 재활성화를 결정하는 숨은 coercion/lifetime 시스템이 + 필요하다. +- 결정: 암묵 약화는 호출 인자 위치의 호출 기간 read-only reborrow만 허용한다. 원래 + exclusive borrow는 원래 last-use까지 유지하며 일반 `let`/대입의 암묵 약화는 에러다. +- 근거: API 호출 편의는 확보하면서 수명 annotation 없이 함수-local R6 분석을 유지한다. +- 기각한 대안: arbitrary implicit reborrow/coercion과 `let s: &T = m` 허용. 대여 종료 + 시점이 숨고 compiler A/B가 별도 coercion graph를 가져야 하므로 기각했다. +- 구현 영향: check/own은 call argument에만 임시 shared view 전이를 만들고 assignment + conversion table에는 추가하지 않는다. + +### R8 provenance lattice + +- 문제: 여러 return path의 static/parameter 파생 결과, optional null 경로와 method의 + 추가 참조 인자를 합칠 명시 규칙이 없으면 caller borrow가 구현 순서에 따라 달라진다. +- 결정: provenance를 `Static`과 `Param(N)`으로 정규화한다. method는 `Param(self)`만, + 자유 함수는 유일한 참조성 parameter만 허용한다. `Static + Param(N)`은 `Param(N)`, + 서로 다른 `Param`의 합류는 에러다. null 경로는 caller borrow가 없는 경로다. +- 근거: provenance가 작은 lattice라 함수 본문만 보고 계산하고 시그니처로 전달할 수 있다. + lifetime annotation이나 interprocedural inference가 필요 없다. +- 기각한 대안: arbitrary parameter union provenance 또는 lifetime parameter. caller에서 + 숨은 alias set/전역 분석이 필요해 Ferro 철학과 맞지 않는다. +- 구현 영향: own.c가 return CFG에서 lattice를 합치고 lowered signature와 `.fei`가 + provenance metadata를 보존한다. + +### M6 branch merge와 loop fixed point + +- 문제: `Owned/Moved`, 초기화 여부와 live borrow가 branch/backedge에서 만날 때 단순히 + 한쪽 상태를 고르면 use-after-move를 놓치거나 안전한 borrow를 너무 일찍 푼다. +- 결정: `Owned + Moved → MaybeMoved`, 경로별 초기화 차이는 `MaybeUninit` 동등 상태로 + 합친다. live borrow는 합집합을 보수적으로 유지하고 incompatible borrow는 약화하지 + 않는다. loop은 진입/종료 상태를 합쳐 두 번째 pass를 돌리고 안정되지 않으면 에러다. +- 근거: 유한한 함수-local lattice와 기존 2-pass만으로 모든 iteration을 보수적으로 + 근사한다. 첫 iteration만 검사하는 불건전성을 피한다. +- 기각한 대안: 첫 pass만 검사, 또는 merge에서 borrow를 `Owned`로 되돌리기. loop-carried + alias와 조건부 move를 놓치므로 기각했다. +- 구현 영향: own.c는 branch exit 전에 last-use를 반영하고 merge table/bit state를 + 구현한다. runtime drop에는 `MaybeMoved` live flag가 필요하다. + +### M7 contextual null/error-union construction + +- 문제: `null`에 독립 타입을 주거나 error union을 일반 implicit conversion으로 다루면 + 타입 추론·overload 후보가 늘고 nominal error 경계가 흐려진다. +- 결정: `null`은 expected optional/pointer-like type이 유일할 때만 구성된다. expected + `E!T` 위치에서는 T가 success, E가 failure를 구성하며 return도 같다. E1/E2 및 + nominal error/`core.Error` 자동 변환은 없다. +- 근거: contextual expected type 한 개만 보면 되어 compiler A/B의 local type checker가 + 결정적이고 nominal error 안전성도 유지된다. +- 기각한 대안: polymorphic null, 일반 union injection conversion, error widening. 숨은 + conversion 및 overload resolution을 요구하므로 기각했다. +- 구현 영향: check는 expected-type 전달 위치에서만 null/error construction을 허용하고 + 문맥 없는 `let p = null`을 진단한다. + +### M7 error declaration uniqueness + +- 문제: code 0 외에도 한 nominal error 선언 안의 중복 member 이름이나 숫자 code는 + match/format 결과를 모호하게 만든다. +- 결정: code 0, 중복 이름, 중복 숫자 code를 모두 compile error로 한다. 서로 다른 nominal + error 선언끼리는 같은 숫자를 사용할 수 있지만 타입은 계속 다르다. +- 근거: 선언 하나의 symbol/code table만 검사하면 되고 runtime representation은 바뀌지 + 않는다. +- 구현 영향: error declaration check가 두 deterministic set을 만들고 중복 위치를 note로 + 표시한다. + +### M7 lazy recovery operators / non-Copy extraction + +- 문제: `orelse`/`catch` RHS를 eager 평가하면 불필요한 side effect와 move가 생긴다. + 또한 `Some(x)` pattern이나 projection이 non-Copy payload를 암묵 이동하면 R7과 + 조건부 drop이 불명확해진다. +- 결정: recovery RHS/handler는 failure 경로에서만 평가한다. optional pattern은 place의 + borrow/view이고 Copy만 복사한다. non-Copy owned payload 추출은 `mem.replace`로만 하며 + temporary optional 자동 추출 예외도 두지 않는다. +- 근거: 평가와 소유권 효과가 같은 CFG 경로를 따르고 기존 projection/R7 상태 기계를 + 재사용한다. +- 기각한 대안: pattern별 destructive move와 temporary 특례. hidden move와 추가 drop + 상태를 만들고 source에서 비용이 보이지 않아 기각했다. +- 구현 영향: lower는 lazy branch를 만들고 own은 실행 경로별 effect를 합친다. pattern + binding은 place mutability에 따른 shared/mutable borrow다. + +### Hierarchical dotted unit namespace + +- 문제: 단일 `unit foo` namespace는 외부 source library가 늘 때 `util`, `types`, `parse` + 같은 이름 충돌을 피할 수 없다. +- 결정: `unit_path := ident ('.' ident)*`, `import unit_path [as ident]`의 계층형 canonical + 이름을 도입한다. import binding은 마지막 segment이고 항상 `binding.member`로 접근한다. +- 근거: Go와 비슷한 단순 unit 전체 import를 유지하면서 namespace 충돌만 해결한다. + resolver에는 relative scope walk나 symbol import가 필요 없다. +- 기각한 대안: relative/glob/selective imports, re-export, friend/package-private visibility. + 이름 해석과 캐시 의존 graph가 복잡해져 v0.1에서 제외했다. +- 구현 영향: lexer keyword 추가는 없고 parser/resolve/diagnostic이 canonical dotted path와 + optional alias를 보존한다. + +### DOS-safe unit naming + +- 문제: host의 case sensitivity와 FAT 8.3 규칙이 다르면 같은 source tree가 다른 unit을 + 찾거나 긴 이름 전송 시 변형될 수 있다. +- 결정: unit segment는 lowercase ASCII, 첫 글자 letter, 이후 letter/digit/underscore, + 최대 8자로 제한한다. dotted path는 root 아래 `segment/.../last.fe`와 정확히 대응하고 + 비교는 규범적 ASCII case-insensitive mapping을 쓴다. +- 근거: unit identity가 DOS와 host에서 같고 8.3 alias 생성에 기대지 않는다. +- 기각한 대안: 일반 Ferro identifier/임의 길이 허용 후 host별 normalization. case-fold와 + truncation 충돌이 platform-dependent라 기각했다. +- 구현 영향: entry path suffix로 project root를 계산하고 mismatch/case-variant duplicate를 + 진단한다. canonical identity는 항상 lowercase dotted form이다. + +### Deterministic import-root resolution / ambiguity rejection + +- 문제: project root, 여러 `-I`, std root를 first-match-wins로 검색하면 `-I` 순서나 host + directory 상태가 실제 선택 source를 바꾼다. +- 결정: 모든 candidate root를 조사하고 동일 unit에 서로 다른 canonical file이 둘 이상 + 있으면 ambiguous error와 path note를 낸다. 같은 실제 file alias만 dedup한다. +- 근거: build/order/platform과 무관한 source graph를 만들어 `.fei`, generated C와 M12 + fixpoint를 안정시킨다. +- 기각한 대안: first-match-wins. 편하지만 shadowing이 command-line order에 숨어 결정성을 + 깨므로 기각했다. +- 구현 영향: driver는 후보를 canonicalize·정렬한 뒤 identity를 비교하고 첫 성공에서 + 검색을 중단하지 않는다. + +### Reserved std namespace + +- 문제: flat `io`, `mem`, `fmt`, `sys`는 user library 이름과 충돌하고 compiler 내장 std + root를 일반 user root처럼 검색하면 같은 이름이 환경에 따라 shadow된다. +- 결정: `std` top-level을 compiler-reserved로 하고 `std.io`, `std.mem`, `std.fmt`, + `std.sys`를 canonical unit으로 쓴다. 마지막 segment binding 때문에 사용 표면은 + `io.write`, `mem.replace`로 유지한다. `str`은 import unit이 아니다. +- 근거: std lookup이 명시적이고 deterministic이며 향후 source package와 충돌하지 않는다. +- 구현 영향: M8에서 std source layout/unit 선언을 이동하고 builtin std root는 `std.*`에만 + 후보가 된다. + +### Source-only external libraries + +- 문제: v0.1에서 package manifest/solver나 stable binary ABI까지 정의하면 M8 범위를 넘어 + `.fei` encoding과 target C ABI를 영구 호환 계약으로 굳히게 된다. +- 결정: 외부 library는 source tree를 `-I` root로 제공한다. package manager/registry/version + solver/manifest 문법과 `.fei + .obj/.lib` binary-only 배포 ABI는 지원하지 않는다. +- 근거: 언어 import 의미는 작게 유지하고 target/model별로 source에서 결정적으로 + 재컴파일할 수 있다. DOS 배포 도구도 단순하다. +- 기각한 대안: package manager를 언어 의미론에 결합, binary-only ABI. compiler A와 + M12 전에 해결할 필요가 없고 호환 부담이 커 기각했다. +- 구현 영향: `-I`는 source-only candidate root이며 미래 도구도 root 구성만 담당한다. + +### `.fei` interface/cache role과 deterministic schema + +- 문제: source hash, public interface hash와 compile cache key가 섞여 있었고 `.fei`의 + 최소 논리 정보·결정적 직렬화 조건이 없어 private 변경이 전체 rebuild를 유발하거나 + host path/timestamp가 fixpoint에 섞일 수 있었다. +- 결정: 세 hash 개념을 분리하고 `.fei`에 version/target/model, canonical unit, public + signatures/layout, anonymous errors, exported generic body/support closure, direct dependency + names/hashes를 기록한다. canonical key 순으로 직렬화하며 absolute path/time/build dir를 + 금지한다. +- 근거: private non-generic 변경은 자기 unit만 재컴파일하고 interface가 같으면 dependent를 + 유지할 수 있다. 같은 graph+target의 `.fei`는 host/build order와 무관하게 byte-identical하다. +- 기각한 대안: source hash를 interface hash로 재사용, unordered serializer. 구현은 짧지만 + 불필요한 rebuild와 M12 비결정성을 만들어 기각했다. +- 구현 영향: `.fei` encoding은 자유지만 논리 schema와 sorted serialization을 만족하고 + driver cache가 dependency interface hash를 사용해야 한다. + +### Ferro visibility vs backend linkage + +- 문제: Ferro private를 무조건 C `static`으로 방출하면 통합 `fe_generics.c`의 exported + instance가 definition unit private helper를 호출할 수 없다. +- 결정: Ferro private는 resolver visibility일 뿐 C linkage와 동일하지 않다. generic + support에 필요한 private top-level symbol은 deterministic unit-mangled linkage와 internal + prototype을 가질 수 있다. +- 근거: source 접근 권한은 유지하면서 단일 통합 generic body 방출을 가능하게 한다. +- 기각한 대안: `C static == Ferro private`, 또는 private helper를 instance마다 복제. + 전자는 linkage 실패, 후자는 중복과 비결정적 출력 때문에 기각했다. +- 구현 영향: resolver는 여전히 cross-unit private 참조를 거부하고 backend/internal header만 + `.fei` support metadata를 통해 symbol을 연결한다. + +### M9 type-only generics + +- 문제: user comptime value generic까지 허용하면 값 canonicalization, mangling, expression + evaluator와 instance explosion 정책을 M9에서 함께 설계해야 한다. +- 결정: v0.1 user generic parameter는 `type`만 지원한다. `struct Box(T)`는 + `comptime T: type` shorthand이며 builtin comptime value와 구별한다. +- 근거: List/Map과 compiler B에 필요한 추상화를 충족하면서 instance key를 canonical type + list로 제한한다. trait/bound도 추가하지 않는다. +- 기각한 대안: integer/string/bool value generics. M11 필수 기능이 아니고 구현/결정성 + 부담이 커 v0.1 이후로 미룬다. +- 구현 영향: parser/check가 user generic parameter type을 제한하고 value generic fixture를 + 명시적으로 거부한다. + +### M9 no type inference + +- 문제: `id(3)`에서 T를 추론하려면 argument constraints, conversion 후보와 향후 overload + 규칙을 정의해야 하고 진단/instance 발견 순서도 복잡해진다. +- 결정: generic type argument는 `id(i32, 3)`처럼 항상 명시한다. compiler-known + `mem.create(value)` inference는 별도 intrinsic 규칙이다. +- 근거: call syntax만 보고 instance key가 결정되어 compiler A/B가 단순하고 deterministic하다. +- 기각한 대안: generic type inference. 편의보다 숨은 constraint solver 비용이 커 기각했다. +- 구현 영향: generic call arity/type argument check는 명시 목록만 검사하며 inference + fallback을 시도하지 않는다. + +### Definition-site resolution + +- 문제: generic body의 non-dependent 이름을 caller scope에서 다시 찾으면 caller의 import와 + shadowing에 따라 같은 generic이 다른 코드를 만든다. +- 결정: 이름은 definition unit에서 고정하고 type-dependent operation만 instantiation 때 + 검사한다. `comptime if`의 선택되지 않은 branch는 parse만 하고 semantic 처리하지 않는다. +- 근거: lexical 의미와 private support를 유지하고 caller/build order와 무관한 instance를 + 만든다. +- 기각한 대안: use-site lookup과 selected-out branch의 eager type check. 전자는 의미가 + 불안정하고 후자는 type-specific branch를 불가능하게 해 기각했다. +- 구현 영향: `.fei`가 body token과 definition-scope symbol/support identity를 전달하고 + generic.c가 그 환경에서 재검사한다. + +### Deterministic monomorphization + +- 문제: request 발견 순서대로 instance를 방출하면 unit traversal/hash iteration/parallel + build에 따라 `fe_generics.c`와 symbol 순서가 바뀐다. +- 결정: canonical definition unit + declaration identity + normalized canonical type args를 + key로 dedup하고 byte ordering으로 정렬한다. prototype 전부를 먼저, body 전부를 나중에 + 같은 canonical 순서로 방출한다. +- 근거: alias instance가 중복되지 않고 recursion/cross-instance call을 지원하며 M12에서 + byte-identical output을 만든다. +- 기각한 대안: first-request order 또는 pointer/insertion-order key. platform/build order에 + 의존해 기각했다. +- 구현 영향: driver/generic.c가 global request set을 정렬하고 alias를 underlying interned + identity로 normalize한다. + +### Generic private-support closure + +- 문제: exported generic이 private helper/type/const/private generic을 참조하면 signature만 + 담은 `.fei`로 다른 unit에서 안전하게 instantiate할 수 없다. +- 결정: `.fei`는 필요한 private support dependency의 transitive closure를 compiler-only + metadata로 제공한다. 이는 Ferro visibility를 public으로 바꾸지 않는다. +- 근거: definition-site semantics와 source private API를 동시에 지키며 `fe_generics.c`에서 + 정확한 backend symbol/layout을 사용할 수 있다. +- 기각한 대안: 모든 support를 source `pub`으로 강제하거나 generic body를 definition unit마다 + static 복제. API 누출 또는 중복/링크 문제 때문에 기각했다. +- 구현 영향: interface hash는 exported generic이 관찰하는 support 변화에 반응하고 + serializer는 closure를 canonical 순서로 기록한다. + +### Recursive instantiation semantics + +- 문제: 단순 재귀 호출이 같은 instance를 다시 요청할 때마다 depth를 올리면 정상 generic + recursion도 limit에 걸리고, 반대로 growing type chain을 dedup만으로 허용하면 무한 생성된다. +- 결정: pending/known 동일 key 재요청은 재사용하고 depth를 소비하지 않는다. 새로운 distinct + instance chain만 증가시키며 32 초과를 에러로 한다. +- 근거: ordinary recursion은 prototype-first 방출로 처리하고 실제 instance explosion만 + 유한한 local driver 상태로 차단한다. +- 구현 영향: generic.c는 pending/known set과 distinct chain stack을 구별하고 최초/현재 + instantiation 위치를 note로 출력한다. + +### Canonical C mangling and type identity + +- 문제: unit이 계층화되고 generic이 통합 방출되면 host path, pointer address 또는 insertion + order 기반 이름은 충돌하거나 run마다 달라질 수 있다. +- 결정: mangling은 canonical dotted unit + declaration + normalized canonical type args만 + 사용한다. nominal identity는 fully-qualified defining unit+name, alias는 underlying identity다. +- 근거: collision-free backend linkage와 M12 fixpoint를 동시에 보장한다. +- 구현 영향: 정확한 escaping 문자는 구현 세부지만 deterministic separator encoding과 + collision 검사가 필요하며 absolute path/address를 symbol에 포함할 수 없다. diff --git a/SPEC.md b/SPEC.md index 80a869a..5941711 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,4 +1,4 @@ -# Ferro 언어 명세 v0.1.7 +# Ferro 언어 명세 v0.1.8 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. @@ -31,7 +31,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 - CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. -- 표준 라이브러리는 코어 공용, `sys` 유닛만 타깃별 구현. +- 표준 라이브러리는 코어 공용, `std.sys` 유닛만 타깃별 구현. --- @@ -95,7 +95,7 @@ and or not orelse - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. `let` 배열·공유 슬라이스에서는 `[]T`, `var` 배열·배타 슬라이스에서는 `[]mut T`가 생긴다. -- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 원래 대여 상태는 배타로 유지되며 다른 저장·대입에는 명시적 `as`가 필요하다. +- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. - `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. - `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 C 표현은 없다. @@ -144,10 +144,12 @@ let v = mem.replace(&mut p, null).?; // 소유값을 실제로 꺼냄 ``` - `?T`에서 T가 일반 `^T`, `&T`, `*T`, `fn`이면 널 포인터를 널 표현으로 사용한다. `?^[]T`는 빈 소유 버퍼와 null을 구별해야 하므로 `(bool, ^[]T)` 표현을 사용한다. +- `null`은 독립 runtime 타입이 없다. expected type으로 정확한 optional 또는 pointer-like 타입을 하나 결정할 수 있는 위치에서만 허용한다. `let p: ?^Node = null;`과 `takes_optional(null);`은 허용하지만 `let p = null;`처럼 문맥이 없거나 둘 이상의 타입으로 해석 가능한 경우는 컴파일 에러다. - 검사 없이 역참조 불가. `p.^`는 컴파일 에러, `p.?.^`가 필요. - `.?`, `.field`, `[i]`는 place projection이다. projection chain은 값을 이동하지 않는다. Copy 값은 읽기에서 복사되며 비-Copy 값을 projection에서 꺼내는 것은 R7에 따라 금지한다. 실제 추출은 `mem.replace`를 사용한다. - `orelse`는 Copy payload를 복사한다. 비-Copy optional 변수 자체에 적용하면 optional 전체를 이동하며, field/index projection의 비-Copy optional에는 직접 적용할 수 없다. -- `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. +- `orelse`는 optional이 `Some`이면 우변을 평가하지 않고, `None`일 때만 우변을 평가하는 lazy 연산이다. 부수 효과·이동·대여도 실행되는 경로에만 적용한다. +- `Some`과 `None`은 `if let`과 `match`의 패턴 위치에서만 옵셔널 해체를 의미하는 문맥 키워드다. 다른 위치에서는 일반 식별자이며 §3의 예약어가 아니다. 패턴은 place를 파괴적으로 추출하지 않는다. immutable place의 payload binding은 shared borrow/view, mutable place는 필요한 mutable borrow/view이고 Copy payload만 복사할 수 있다. non-Copy payload의 소유권을 꺼내려면 `mem.replace`가 필요하며 temporary optional만 자동 소유 추출하는 예외도 없다. ### 4.6 에러 @@ -165,15 +167,18 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 } ``` -- `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약, 사용 불가. +- `error` 선언은 `u16` 코드 집합. 코드 0은 "성공" 예약이라 사용할 수 없고, 한 선언 안에서 member 이름이나 숫자 code가 중복되면 컴파일 에러다. 서로 다른 nominal error 선언은 같은 숫자 code를 사용할 수 있지만 여전히 다른 타입이다. +- expected type이 `E!T`인 위치에서는 `T` 값은 success, `E` 값은 failure를 구성한다. 함수의 `return`도 선언된 반환 타입이 `E!T`이면 같은 규칙을 쓴다. 이는 일반 implicit conversion이 아니라 error-union 전용 contextual construction이며, `E1`과 `E2` 또는 nominal error와 `core.Error` 사이의 자동 변환은 없다. - `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다. - `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다. - `try e`: 에러면 현재 함수에서 즉시 반환한다. - `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. +- 짧은 `catch`의 우변과 block `catch`의 handler는 피연산자가 error일 때만 평가·실행한다. success이면 handler의 부수 효과·이동·대여가 발생하지 않는다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. +- `try`와 `catch`도 field/index/optional projection에서 non-Copy payload를 숨게 이동시키지 않는다. projection에서 소유값을 추출해야 하면 먼저 `mem.replace`로 유효한 대체값을 남긴다. - 에러는 값이다. 언와인딩, 스택 추적, 소멸자 이외의 자동 정리 없음. -- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1.2에는 별도 `must` 키워드를 두지 않는다. +- 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1에는 별도 `must` 키워드를 두지 않는다. `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. @@ -183,7 +188,9 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 결과가 결정적이며 `switch` case 라벨로 쓸 수 있다. 이름 집합이 바뀌면 `fe_errors.h`와 그 헤더에 의존하는 오브젝트를 무효화하지만, -각 유닛의 `.c`는 재방출하지 않는다. `.fei`는 이름만 기록하므로 무효화되지 않는다. +각 유닛의 `.c`는 재방출하지 않는다. 각 `.fei`는 그 unit이 사용하는 이름 집합만 +기록하므로 global 번호 재배정 때문에 다시 쓰지 않으며, source에서 이름 사용 자체가 +바뀐 unit의 `.fei` interface만 갱신한다. 빌드 디렉터리 이력에 따라 번호가 달라지는 append-only 표는 금지한다. 유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 확정된 표를 받아야 하며, 없으면 컴파일 에러다. @@ -229,9 +236,23 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 **R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. +v0.1의 대여 상태는 **root local/parameter 단위**로 추적한다. projection은 place의 root를 찾는 데만 쓰며 field-sensitive/index-sensitive 독립성을 증명하지 않는다. 따라서 `&mut p.a`는 `p` 전체를 배타 대여하고 그동안 `p.b`의 읽기·쓰기·대여도 금지한다. `&mut xs[0]`과 `&mut xs[1]`도 서로 다른 index라는 이유로 분리하지 않고 같은 root `xs`의 충돌 대여로 본다. 이는 compiler A/B의 함수-local 상태 기계를 작고 결정적으로 유지하기 위한 v0.1의 의도적인 보수성이다. + +```fe +var p = Pair{ a: 1, b: 2 }; +let r = &mut p.a; +p.b = 3; // 에러: root p가 배타 대여 중 +r.^ = 4; + +let a = &mut xs[0]; +let b = &mut xs[1]; // 에러: 둘 다 root xs를 대여 +a.^ = 1; +b.^ = 2; +``` + 참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. `defer` 블록에서 사용한 참조와 그 원본의 대여는 해당 defer가 실행되는 스코프 끝까지 연장한다. -호출 인자 위치의 `&mut T → &T`, `[]mut T → []T` 약화는 새 공유 대여가 아니라 기존 배타 대여의 읽기 전용 재대여다. 호출이 끝날 때 재대여만 끝나며 원래 배타 대여의 생존 구간은 유지된다. +호출 인자 위치의 `&mut T → &T`, `[]mut T → []T` 약화는 새 장기 공유 대여가 아니라 기존 배타 대여의 읽기 전용 재대여다. callee를 평가한 뒤 해당 인자를 평가하는 시점부터 호출이 끝날 때까지만 임시 재대여가 존재하고, 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에서는 암묵 약화를 허용하지 않으므로 `let s: &i32 = m;`(`m: &mut i32`)은 컴파일 에러다. 별도의 lifetime/coercion 시스템은 두지 않는다. 이 판정은 함수 지역 liveness 분석이며 함수 밖 정보를 쓰지 않으므로 §1.2를 위반하지 않는다. @@ -250,7 +271,14 @@ x += 1; // OK — 여기서 r의 대여는 이미 끝났다 **(b) 정적 파생.** 반환값이 문자열 리터럴 또는 `static` 선언에서 파생된 경우. 이때는 참조성 파라미터가 없어도 된다. -호출 지점에서 R8(a)의 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. R8(b)의 결과는 대여를 만들지 않는다. +참조성 반환의 provenance는 인터페이스에서 다음 둘로 정규화한다. + +- `Static`: 문자열 리터럴 또는 `static`에서 파생되어 caller local borrow를 만들지 않는다. +- `Param(N)`: 시그니처로 정해진 하나의 참조성 parameter에서 파생된다. 메서드는 `Param(self)`만 허용하며 다른 참조성 인자에서 파생되면 에러다. 자유 함수는 기존 규칙대로 참조성 parameter가 정확히 하나여야 한다. + +control-flow 합류는 `Static + Static → Static`, `Static + Param(N) → Param(N)`, `Param(N) + Param(N) → Param(N)`이다. 서로 다른 `Param` provenance가 합류하면 컴파일 에러다. `?&T`/`?[]T`의 `null` 반환 경로는 caller borrow를 만들지 않는 경로이므로 static/null 경로와 `Param(N)` 경로가 합쳐지면 전체를 보수적으로 `Param(N)`으로 본다. 이 provenance는 함수 시그니처와 lowered `.fei` interface metadata에 기록할 수 있어야 한다. + +호출 지점에서 `Param(N)` 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. `Static` 결과는 caller local borrow를 만들지 않는다. ```fe let t = line.trim(); // 내장 alias 메서드 R8(a) // OK. line은 t의 대여 구간 동안 잠긴다 @@ -282,7 +310,7 @@ fn f(r: &mut i32) { G = 5; } // r과 G가 같은 곳을 가리키는지 f는 fn g() { f(&mut G); } // 제한이 없으면 g의 지역 검사는 통과한다 ``` -- 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1.2에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. +- 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. **R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. @@ -293,8 +321,9 @@ fn g() { f(&mut G); } // 제한이 없으면 g의 지역 검사는 통 ### 6.1 EBNF ``` -unit := 'unit' ident ';' import* decl* -import := 'import' ident ';' +unit := 'unit' unit_path ';' import* decl* +unit_path := ident ('.' ident)* +import := 'import' unit_path ['as' ident] ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl | const_decl | global_decl) | comptime_decl @@ -344,14 +373,15 @@ pattern := ident // 배리언트, 페이로드 없음 | 'Some' '(' ident ')' | 'None' | int_literal | char_literal | 'true' | 'false' | '_' -type := ident ['.' ident] - | '?' type | '!' type | ident '!' type +qualified_name := ident ('.' ident)* +type := qualified_name + | '?' type | '!' type | qualified_name '!' type | '^' type | '&' ['mut'] type | '*' type | 'far' ('^' | '*' | '&' ['mut']) type | 'far' 'fn' '(' [type (',' type)*] ')' ['->' type] | '[' expr ']' type | '[' ']' ['mut'] type | 'fn' '(' [type (',' type)*] ')' ['->' type] - | ident '(' type (',' type)* ')' // 제네릭 인스턴스 + | qualified_name '(' type (',' type)* ')' // 제네릭 인스턴스 catch_expr := expr 'catch' ['|' ident '|'] (expr | block) orelse_expr := expr 'orelse' expr @@ -359,6 +389,7 @@ orelse_expr := expr 'orelse' expr `member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 섞어 쓸 수 있다. `catch`의 블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다. +`unit_path` segment의 lexical 제한과 source path 대응은 §8.1이 규정한다. ### 6.2 표현식 우선순위 (낮음 → 높음) @@ -383,6 +414,8 @@ orelse_expr := expr 'orelse' expr - `and`, `or`는 단축 평가한다. 호스트 C 방출에서는 각각 `&&`, `||`로 매핑하며, 평가 순서와 단락 규칙은 Ferro 의미론을 그대로 유지한다. +- `orelse`와 `catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를 + 평가하지 않는다(§4.5·§4.6). - `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. - `..`는 일반 표현식 연산자가 아니며 `for` 헤더에서만 쓸 수 있다. - 비교 연산 체이닝 금지(`a < b < c`는 에러). @@ -416,7 +449,8 @@ orelse_expr := expr 'orelse' expr ```fe @print("x={} y={x} name={s}\n", a, b, s); ``` -→ lower 단계에서 개념적으로 다음처럼 전개한다: +→ lower 단계에서 개념적으로 다음처럼 전개한다. 아래 `io`와 `fmt`는 각각 canonical +`std.io`, `std.fmt` 유닛을 가리킨다. ``` io.write(out, "x="); var t1: [12]u8 = undefined; io.write(out, fmt.fmt_int_i32(t1[..], a)); @@ -432,7 +466,7 @@ io.write(out, " name="); io.write(out, s); io.write(out, "\n"); - verb: `{}` 기본(정수/bool/char/str 자동), `{x}` 16진, `{c}` 문자, `{s}` 문자열/슬라이스, `{b}` 불린. `{{`는 `{` 이스케이프. - `{}` 개수와 인자 개수 불일치 → 컴파일 에러. - 인자 타입에 대응하는 `fmt.fmt_*` 함수가 없으면 컴파일 에러(메시지에 타입명 표시). -- 자릿수/폭/정렬 지정자는 v0.1.1에 없음. 필요하면 `fmt.fmt_int_pad`를 직접 호출. +- 자릿수/폭/정렬 지정자는 v0.1에 없음. 필요하면 `fmt.fmt_int_pad`를 직접 호출. - `@fprint`의 첫 인자는 Copy 핸들 `io.Writer`(§10)다. - `@print`는 `io.Writer.Stdout`에 기록하며 저수준 writer 오류를 삼키고 `void`를 반환한다. 따라서 `try @print(...)`는 컴파일 에러다. @@ -450,7 +484,7 @@ io.write(out, " name="); io.write(out, s); io.write(out, "\n"); ```fe unit vga; -import sys; +import std.sys; const WIDTH: u16 = 320; const HEIGHT: u16 = 200; @@ -470,7 +504,7 @@ pub fn put_pixel(x: u16, y: u16, c: u8) { ```fe unit main; -import io; +import std.io; fn count_lines(path: str) -> !usize { var f = try io.open(path, io.Read); @@ -547,16 +581,150 @@ pub fn main() -> !void { - 함수의 `comptime` 파라미터는 §9 제네릭. - 재귀 평가 깊이 제한 256, 초과 시 에러. +### 7.6 표현식 평가 순서 + +- 일반 표현식의 평가 순서는 소스의 왼쪽에서 오른쪽이다. +- 함수·메서드 호출은 callee를 먼저 평가하고 인자를 소스 순서대로 왼쪽에서 오른쪽으로 평가한다. 자동 `self` projection도 callee 평가의 일부다. +- 이항 연산자는 왼쪽 operand를 먼저, 오른쪽 operand를 나중에 평가한다. +- `and`와 `or`는 왼쪽 operand로 결과가 정해지면 오른쪽을 평가하지 않는다. `orelse`와 `catch`도 §4.5·§4.6에 따라 우변/handler가 필요한 경로에서만 평가한다. +- 이 순서는 부수 효과뿐 아니라 move, borrow의 시작·마지막 사용, `try` 전파와 defer/drop cleanup 순서를 결정한다. +- C backend는 C 자체의 미지정 평가 순서에 의존할 수 없다. C에서 순서가 보장되지 않는 호출 인자, 일반 이항 operand 등의 부수 효과·이동·대여는 lower 단계에서 순서가 명시된 temporary statement로 분해한다. `and`/`or` 같은 C의 단락 규칙을 직접 사용하더라도 Ferro의 lazy 의미를 그대로 보존해야 한다. + --- ## 8. 유닛과 빌드 -- 파일 하나 = 유닛 하나. 첫 줄은 `unit <이름>;`이며 파일명과 일치해야 함. -- `import bar;` → 같은 검색 경로의 `bar.fe`. 접근은 `bar.name`. -- `pub` 붙은 선언만 외부 노출. 구조체 필드도 개별 `pub` 필요. -- 순환 import 금지(에러). -- 유닛 컴파일 시 `.fei` 생성: pub 선언 시그니처, 타입 레이아웃, 제네릭 본문 토큰과 제네릭 전용 private 심볼 시그니처. 재컴파일 cache key는 해당 소스 해시뿐 아니라 직접·간접 의존 유닛의 `.fei` 해시를 포함한다. -- 검색 경로: `-I `, 기본은 현재 디렉터리 + `/std`. +파일 하나가 유닛 하나다. 유닛의 canonical identity는 fully-qualified dotted unit path이며 +모든 cross-unit 타입·선언·제네릭 identity, `.fei`, C mangling, cache key와 진단에서 같은 +이름을 사용한다. + +```fe +unit game.main; + +import game.render; +import tinyjson.parse; +import net.http as http; +import std.io; +``` + +import는 항상 유닛 전체를 가져오며 member는 local unit binding으로 한정해 접근한다. +기본 binding은 마지막 segment이므로 `import tinyjson.parse;` 뒤에는 +`parse.read(...)`, `import std.io;` 뒤에는 `io.write(...)`를 쓴다. `as`가 있으면 그 +alias가 binding이다. + +v0.1은 relative import(`.foo`, `..foo`), glob/selective import, `pub import` re-export, +package-private/friend visibility를 지원하지 않는다. + +### 8.1 unit 이름과 source path + +unit path의 각 segment는 ASCII lowercase `a`~`z`로 시작하고 이후에는 `a`~`z`, +`0`~`9`, `_`만 쓸 수 있으며 최대 8자다. 일반 Ferro identifier는 계속 case-sensitive이고 +이 제한은 unit path에만 적용한다. `game.main`, `tinyjson.parse`는 허용하지만 +`TinyJson.Parse`, `very_long_library_name`은 unit path로 허용하지 않는다. 이 규칙은 +FAT/DOS 8.3과 case-sensitive host에서 같은 source가 같은 유닛으로 해석되게 한다. + +unit path는 import root 아래의 상대 source path와 정확히 대응한다. canonical identity는 +항상 dotted path이고 실제 path separator만 host/DOS에 맞게 바꾼다. + +``` +tinyjson.parse -> tinyjson/parse.fe +game.world.map -> game/world/map.fe +``` + +entry source의 선언이 `unit game.main;`이고 실제 파일이 +`C:/PROJECT/SRC/GAME/MAIN.FE`이면 file path 끝의 `game/main.fe` suffix를 ASCII +case-insensitive 방식으로 비교해 제거하고 project source root `C:/PROJECT/SRC`를 얻는다. +unit path/file path 대응은 host filesystem의 기본 case 규칙이 아니라 이 규범적 비교를 +써서 DOS와 모든 host에서 같게 처리한다. suffix가 일치하지 않으면 컴파일 에러이며 같은 +unit 선언을 임의 위치에서 조용히 허용하지 않는다. case-sensitive host에 `game/main.fe`와 +`GAME/MAIN.FE`가 별도 실제 파일로 함께 있으면 §8.2의 서로 다른 후보이므로 ambiguous다. + +### 8.2 import root와 모호성 + +candidate root는 다음 집합이다. + +1. entry source에서 계산한 project source root +2. 사용자가 지정한 각 `-I ` +3. `std.*`에 대해서만 compiler 내장 std root + +`std` 최상위 namespace는 compiler-reserved이며 user unit은 선언할 수 없다. 일반 user +unit은 compiler std root에서 찾지 않는다. 표준 유닛은 `import std.io;`, +`import std.mem;`, `import std.fmt;`, `import std.sys;`처럼 가져온다. `str`은 계속 built-in +`[]u8` alias와 alias-method namespace이며 import unit이 아니다. + +root를 순서대로 검사해 첫 성공을 고르지 않고 모든 candidate를 조사한다. 동일 canonical +unit path에 대해 서로 다른 실제 source file이 둘 이상 발견되면 다음 형태의 compile +error를 내고 각 실제 path를 note로 표시한다. + +``` +ambiguous unit 'foo.bar' +note: ... +note: ... +``` + +filesystem canonicalization 결과 같은 실제 파일이 여러 root 또는 path alias로 발견된 +경우만 하나로 취급할 수 있다. 따라서 `-I` 순서, 디렉터리 열거 순서, host 환경이 source +선택을 바꾸지 않는다. + +### 8.3 binding, visibility와 외부 library + +한 unit에서 import binding은 다른 unit-scope declaration/import binding과 충돌할 수 없다. +`import foo.net; import bar.net;`은 둘 다 `net`을 만들므로 에러이며 두 번째를 +`import bar.net as bar_net;`처럼 alias해야 한다. alias는 일반 Ferro identifier다. + +visibility는 private과 `pub` 두 단계뿐이다. private 선언은 같은 unit에서만 보이고, +`pub` 선언과 개별 `pub` field/method만 import한 모든 caller에서 보인다. dotted prefix는 +권한이 아니므로 `game.foo`와 `game.bar`는 서로의 private 선언에 접근할 수 없다. public +function parameter/return, public field 등 외부 signature에 나타나는 nominal type은 +importer가 이름을 해석할 수 있어야 하며 private nominal type을 public API에 노출하면 +컴파일 에러다. + +v0.1 외부 library는 source-only import root다. 예를 들어 `-I deps`와 +`deps/tinyjson/parse.fe`가 있으면 `import tinyjson.parse;`로 사용한다. package manager, +registry, version solver, manifest dependency 문법은 v0.1에 없다. 미래 package manager도 +dependency source tree를 import root에 배치하고 `-I`를 구성하는 도구일 뿐 Ferro import +의미론을 바꾸지 않는다. `.fei + .obj/.lib`만 배포하는 binary-only package ABI도 v0.1은 +지원하지 않는다. + +### 8.4 `.fei` interface와 cache + +`.fei`는 incremental compilation interface, build cache metadata, 다른 unit에서의 generic +instantiation을 위한 compiler interface다. 물리적 binary/text encoding은 구현 세부지만 +논리적으로 최소한 다음을 표현할 수 있어야 한다. + +- magic/format version, Ferro SPEC/compiler interface version +- target과 해당하는 경우 bits16 memory model +- canonical unit name +- public symbol signature와 public nominal type identity/layout +- anonymous `error.Name` name set +- exported generic declaration metadata와 body token stream +- exported generic이 요구하는 private support symbol metadata +- direct dependency canonical unit name과 dependency interface hash + +`.fei` serialization은 결정적이어야 한다. unordered container iteration을 그대로 쓰지 +않고 canonical key/name의 byte ordering으로 정렬해 serialize한다. absolute host path, +timestamp, build directory를 기록하지 않는다. 같은 source/interface graph와 target/model이면 +build order와 host path에 관계없이 byte-identical `.fei`가 목표다. + +다음 hash는 구별한다. + +- **source hash**: 해당 unit source 내용 변화 감지 +- **interface hash**: dependent unit이 관찰하는 `.fei` 의미 정보의 hash +- **compile cache key**: source hash, target/model/options와 실제 재컴파일에 필요한 + direct/indirect dependency interface hash를 포함한 key + +private non-generic 구현만 바뀌어 public/generic-visible interface가 같으면 해당 unit은 +재컴파일하지만 interface hash는 유지되어 dependent unit을 재컴파일하지 않아도 된다. +public signature/layout 또는 exported generic이 관찰하는 private support 정보가 바뀌면 +interface hash가 바뀐다. + +### 8.5 순환과 canonical identity + +순환 import는 컴파일 에러다. fully-qualified dotted unit path와 선언 이름이 nominal +identity의 기준이므로 `tinyjson.value.Value`와 `tinyjson.value.Box`처럼 표시한다. nominal +struct/enum/error는 defining unit + declaration name으로 구별되고 type alias는 새 nominal +identity를 만들지 않는다. 이 canonical identity 규칙은 §9 generic cache와 §11.4 C +mangling에도 그대로 적용한다. ``` fec main.fe --target=bits32 -o game.exe @@ -565,14 +733,14 @@ fec main.fe --emit-c -o out/ # 트랜스파일 결과만 fec --dump-ast main.fe ``` -### 8.1 CLI 플래그 (전체) +### 8.6 CLI 플래그 (전체) | 플래그 | 의미 | 규정 | |---|---|---| | `--target=bits16\|bits32` | 타깃 선택 | §2 | | `--model=small\|large` | `bits16` 메모리 모델 | §2 | | `-o <경로>` | 출력 파일 또는 디렉터리 | §8 | -| `-I <디렉터리>` | 유닛 검색 경로 추가 | §8 | +| `-I <디렉터리>` | source-only import candidate root 추가 | §8.2·§8.3 | | `--emit-c` | 트랜스파일 결과만 생성 | §8 | | `--dump-ast` | AST 덤프 | §8 | | `--no-checks` | 경계·오버플로·`.?` 검사 제거 | §7.4 | @@ -584,7 +752,8 @@ fec --dump-ast main.fe ## 9. 제네릭 -`comptime` 파라미터 기반 모노모피제이션. +`comptime` type 파라미터 기반 모노모피제이션. v0.1의 user-defined generic parameter는 +`type`만 지원한다. ```fe pub struct List(T) { @@ -607,24 +776,75 @@ let m = max(i32, 3, 7); var xs: List(u8) = List(u8).new(); ``` -- 인스턴스화 시 타입 인자를 대입해 본문을 재검사하고 코드를 생성한다. 인스턴스 캐시 키는 `(선언, 타입 인자 목록)`. -- 제약(trait bound) 없음. 본문에서 쓰는 연산이 그 타입에 없으면 **인스턴스화 시점에** 에러(에러 메시지에 인스턴스화 위치를 표시할 것). -- 이름 해석은 항상 정의 유닛의 스코프에서 한다. `.fei`에 제네릭 본문 토큰과 본문이 참조하는 private 심볼의 제네릭 전용 시그니처를 저장한다. 이 표시는 Ferro source의 `pub` 접근 권한을 넓히지 않는다. -- 최종 build driver는 모든 사용 유닛의 인스턴스 요청을 합치고 중복 제거해 단일 `fe_generics.c`에 방출한다. 사용 유닛별 external 중복 심볼이나 `static` 코드 복제를 만들지 않는다. -- comptime에서 type 값의 `==`/`!=`, `@is_int(T)`, `@is_ptr(T)`를 허용한다. 타입 인터닝 identity로 평가하며 런타임 type reflection은 없다. -- 재귀적 인스턴스화 깊이 제한 32. +- `fn id(comptime T: type, x: T) -> T`를 기본형으로 하며 `struct Box(T)`와 + `enum Maybe(T)`의 `T`는 `comptime T: type`의 shorthand다. `comptime N: usize`, + comptime string/bool 등 user value generic은 지원하지 않는다. compiler builtin의 기존 + comptime value는 user generic parameter가 아니다. +- generic type argument는 항상 명시한다. `id(i32, 3)`은 허용하지만 `id(3)`에서 T를 + 추론하지 않는다. `mem.create(value)`처럼 별도로 정의된 compiler-known intrinsic + inference는 일반 generic inference가 아니다. +- generic struct/enum의 method는 enclosing type parameter를 사용할 수 있다. 그러나 + method/function이 enclosing type parameter 외에 별도의 새 generic parameter list를 + 선언하는 generic-method 기능은 v0.1에 없다. +- 인스턴스화 시 타입 인자를 대입해 type-dependent operation을 재검사하고 코드를 + 생성한다. trait/bound와 overload resolution은 없다. 본문 연산이 해당 타입에서 invalid면 + definition/body의 실제 연산 위치를 primary error로 표시하고 각 caller에 `instantiated + here` note를 붙인다. nested instance는 가능한 범위에서 instantiation chain을 표시한다. +- generic body의 이름은 항상 definition unit scope에서 해석한다. non-dependent name은 + 정의 시 그 symbol로 고정되며 caller의 같은 이름은 영향을 주지 않는다. private support + symbol도 definition unit의 것을 쓴다. `comptime if`의 선택되지 않는 branch는 parse만 + 하고 semantic name resolution/type checking/codegen을 하지 않는다. +- generic instance의 canonical key는 **canonical definition unit + canonical declaration + identity + canonical type argument list**다. type alias는 새 nominal identity가 아니므로 + underlying/interned canonical type identity로 정규화한다. 따라서 `const Word = i32;` 뒤의 + `id(Word, 1)`과 `id(i32, 2)`는 같은 instance다. +- 최종 build driver는 모든 unit의 instance request를 모아 canonical key로 중복 제거하고 + key의 byte ordering으로 정렬한다. 먼저 필요한 prototype을 결정적 순서로 방출하고 이어서 + body를 같은 순서로 단일 `fe_generics.c`에 방출한다. request 발견 순서, hash iteration, + build order에 의존하거나 사용 unit별 external/static 중복 코드를 만들지 않는다. +- exported generic이 definition unit의 private symbol을 참조하면 `.fei`는 다른 unit에서 + instantiate하는 데 필요한 support dependency의 transitive closure를 기록한다. private + non-generic function은 signature와 backend link identity, private nominal type은 필요한 + identity/layout/signature, comptime const는 evaluated value/type, private generic은 body + token stream과 자기 support dependency를 제공한다. 이 compiler/link metadata는 Ferro + source visibility를 public으로 바꾸지 않는다. + +```fe +unit lib; + +fn helper(x: i32) -> i32 { return x + 1; } + +pub fn bump(comptime T: type, x: T) -> T { + comptime if T == i32 { return helper(x); } + return x; +} +``` + +다른 unit이 요청한 `lib.bump(i32)` instance는 generated internal C symbol을 통해 +`helper`를 호출할 수 있지만, 다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다. + +- 재귀적 인스턴스화의 distinct-instance chain 제한은 32다. 이미 pending/known인 동일 + canonical instance key를 다시 요청하는 recursion은 pending instance를 재사용하고 depth를 + 소비하지 않는다. 새로운 distinct instance가 연쇄적으로 생길 때만 depth가 증가하며 + 32를 초과하면 최초/현재 위치와 instance chain을 포함한 compile error를 낸다. +- comptime에서 type 값의 `==`/`!=`, `@is_int(T)`, `@is_ptr(T)`를 허용한다. canonical + interned type identity로 평가하며 런타임 type reflection은 없다. --- ## 10. 표준 라이브러리 (최소 집합) -- **core**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. -- **mem**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다. +표준 라이브러리는 reserved `std` namespace 아래에 있으며 `import std.io;`처럼 명시적으로 +가져온다. import 뒤의 local binding은 마지막 segment라 기존처럼 `io.write`, `mem.replace` +형태로 사용한다. 실제 `fec/std` source 배치는 M8에서 이 canonical unit path에 맞춘다. + +- **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. +- **`std.mem`**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다. - **문자열/바이트**: `str`은 `[]u8` alias다. 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr`를 `line.trim()`처럼 호출하며 `str` 이름의 import 유닛은 두지 않는다. 소유 문자열 `String`은 `^[]u8`을 감싸고 `as_str(self: &Self) -> str`을 제공한다. -- **list**: `List(T)`. -- **map**: `Map(K, V)`(오픈 어드레싱, K는 정수 또는 `String`). `String` key map은 key buffer를 소유하고 조회에는 `get_str(self: &Self, key: str) -> ?&V`를 제공한다. -- **fmt**: sink를 소유하지 않는 순수 변환 함수 모음. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error`는 `--strip-error-names`를 따른다. -- **io**: +- **`std.list`**: `List(T)`. +- **`std.map`**: `Map(K, V)`(오픈 어드레싱, K는 정수 또는 `String`). `String` key map은 key buffer를 소유하고 조회에는 `get_str(self: &Self, key: str) -> ?&V`를 제공한다. +- **`std.fmt`**: sink를 소유하지 않는 순수 변환 함수 모음. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error`는 `--strip-error-names`를 따른다. +- **`std.io`**: ```fe pub enum Writer { Stdout, Stderr, File(u16), Null } pub enum Reader { Stdin, File(u16) } @@ -632,8 +852,8 @@ var xs: List(u8) = List(u8).new(); 둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다. `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다. 닫힌 fd 또는 재사용된 fd를 가진 복사 handle은 I/O 오류나 의도하지 않은 파일 접근이라는 논리 오류를 만들 수 있지만 dangling memory access는 만들지 않는다. - `File{ open, create, read(self: &mut Self, []mut u8), write(self: &mut Self, []u8), seek, size, writer, reader, close }`. - `close(self: Self) -> !void`는 File을 소비하는 일반 메서드이며 `drop`이 아니다. 내부 handle을 먼저 invalid 상태로 만든 뒤 닫기 오류를 반환하므로 함수 종료의 자동 drop은 no-op이다. `drop`은 아직 열린 handle만 오류를 무시하고 닫는다. `drop` 직접 호출 금지는 유지한다. - - 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 반환해서는 안 된다. 따라서 v0.1.2에는 함수 포인터/`*void` 기반 Writer·Reader나 buffer Writer가 없다. `@sprint`는 대상 slice에 직접 복사한다. -- **sys**: `exit`, `on_exit(f: fn() -> void) -> !void`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). `on_exit`은 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. + - 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 반환해서는 안 된다. 따라서 v0.1에는 함수 포인터/`*void` 기반 Writer·Reader나 buffer Writer가 없다. `@sprint`는 대상 slice에 직접 복사한다. +- **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). `on_exit`은 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. --- @@ -703,16 +923,28 @@ fec/ | `?T` (그 외) | `struct { unsigned char has; T v; }` | | `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | | `shared [atomic] var x: T` | `volatile T x` | -| struct | `struct fe__` | +| struct | `struct fe__` | | enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` | -| 함수 | `fe__`, 메서드는 `fe___` | -| 제네릭 인스턴스 | `fe____<타입인자맹글>` | +| 함수 | `fe__`, 메서드는 `fe___` | +| 제네릭 인스턴스 | `fe____` | 세부: +- **canonical mangling**: C symbol은 canonical dotted unit path + declaration name + canonical + type argument list를 collision-free하게 encode한다. `.`의 separator/escaping 문자는 구현 + 세부지만 host path, pointer address, insertion/hash iteration order를 사용할 수 없다. + type alias는 canonical underlying identity를 쓰고 nominal struct/enum/error는 fully-qualified + defining unit + name을 쓴다. generic instance 이름도 같은 key에서만 생성해 M12 fixpoint에서 + byte-identical해야 한다. +- **Ferro visibility와 C linkage**: Ferro `private`는 source name visibility이며 반드시 C + `static`을 뜻하지 않는다. exported generic instance가 definition unit의 private helper를 + 호출할 수 있도록 generic body가 필요로 하는 private top-level function/global을 + deterministic unit-mangled external C symbol로 방출하고 내부 generated header에 prototype을 + 제공할 수 있다. 이는 resolver의 private 접근을 완화하지 않으며 다른 Ferro unit source의 + 직접 참조는 계속 에러다. 필요한 closure는 §9와 `.fei` metadata가 제공한다. - **오버플로 검사**: `fe_add_i16(a, b, LINE)` 인라인 함수. `--no-checks`면 매크로가 `((a)+(b))`로 축약. - **경계 검사**: `fe_idx_T(s, i, LINE)` → `(i < s.n ? s.p[i] : (fe_trap_bounds(LINE), s.p[0]))`. `for` 루프는 직접 인덱스. - **`try`**: `{ Ttmp t = expr; if (t.e) return (RetT){ t.e }; }` 후 `t.v` 사용. defer/소멸자가 있으면 return 전에 정리 코드 삽입. -- **`catch`**: `t.e`가 참일 때 블록 실행, 바인딩 변수는 `t.e`. +- **`catch`**: `t.e`가 참일 때만 block 또는 짧은 RHS를 평가하고 바인딩 변수는 `t.e`다. - **`defer`/소멸자**: lower 단계에서 스코프 종료 지점(정상 흐름, `return`, `break`, `continue`, `try` 전파)마다 역순 호출을 명시적으로 삽입. C의 goto 라벨을 써도 되고 복제해도 된다(A는 복제, B는 goto 권장). - **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. - **`match`**: `switch (x.tag)`. Copy payload는 지역 변수로 복사하고 non-Copy projection payload는 R7에 따라 참조로만 바인딩한다. 소유값 추출은 match 전 `mem.replace`로 수행한다. @@ -721,8 +953,15 @@ fec/ - **참조와 aliasing**: `&T` → `const T*` 방출은 aliasing 가정을 하지 않는다. `&mut T`에도 `restrict`를 붙이지 않으며, M13/M14 네이티브 백엔드도 noalias를 가정하지 않는다. R6의 배타성은 R10의 전역 대여 금지가 함께 성립할 때만 프로그램 전체에서 유지되므로, 방출 단계에서 이를 최적화 근거로 쓰지 않는다. - **에러 코드**: 드라이버가 emit 전에 확정한 `error.Name`의 `u16` 코드를 단일 `fe_errors.h`의 `#define`으로 방출한다(§4.6). 모든 유닛 C가 이 헤더를 include하므로 에러 `match`를 `switch`로 방출할 수 있고 이름 집합 변경 시 C 재방출 없이 오브젝트만 무효화한다. - **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. +- **평가 순서**: §7.6의 callee-first, operand/argument left-to-right 순서를 지킨다. C가 + 순서를 보장하지 않는 일반 호출 인자와 이항 operand에 부수 효과·move·borrow가 있으면 + lower가 순서대로 temporary statement를 만들고 emit은 그 결과만 조합한다. cleanup과 + `try` 전파도 같은 순서를 따른다. - **공유 상태와 임계 구역**: `shared`는 `volatile`로 방출한다. bits16의 `critical`은 compiler barrier → FLAGS 저장 → `cli` 순서로 진입하고 모든 이탈에서 저장한 FLAGS 복원 → compiler barrier 순서로 끝낸다. GCC 계열은 `asm volatile("" ::: "memory")`, Open Watcom/Borland는 optimizer가 내용을 볼 수 없는 별도 runtime 함수 호출 경계를 사용한다. 한 명령 크기의 `shared atomic` 단일 접근은 volatile load/store만 방출한다. -- **방출 순서**: `fe_errors.h` → typedef 전방선언 → struct 정의(의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문 → 통합 `fe_generics.c`. +- **방출 순서**: `fe_errors.h` → typedef 전방선언 → struct 정의(동률을 canonical name으로 + 끊는 의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문 → 통합 `fe_generics.c`. + unordered container나 source 발견 순서에 기대지 않는다. `fe_generics.c`는 canonical + instance prototype을 먼저, body를 나중에 각각 key byte ordering으로 방출한다. - 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성한다. 제네릭 인스턴스 본문은 유닛 C에 중복 방출하지 않는다. ### 11.5 own.c 알고리즘 @@ -732,16 +971,45 @@ fec/ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive ``` +초기화 여부가 경로마다 다른 합류에는 `MaybeUninit` 또는 같은 의미의 별도 bit/state를 +사용할 수 있다. projection은 root local/parameter를 찾는 데만 사용하고 field/index별 +대여 상태는 만들지 않는다(R6). + 1. 먼저 AST를 역방향 순회해 각 참조 변수의 경로별 마지막 사용을 계산한다. `defer` 안의 사용은 해당 스코프 끝으로 올린다. -2. AST를 문장 순서로 순회하며 상태 전이한다. 표현식의 place 사용을 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기 / projection으로 분류한다. +2. AST를 §7.6의 평가 순서로 순회하며 상태 전이한다. 표현식의 place 사용을 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기 / projection으로 분류하고 projection의 root 상태를 갱신한다. 3. 이동: `Owned → Moved`. `Moved`/`MaybeMoved` 사용 시 에러(최초 이동 위치를 표시). field/index/`.?` projection의 비-Copy 이동은 R7에 따라 거부하고 `mem.replace`만 허용한다. -4. `&x`: `Owned → Shared(n+1)`. `&mut x`: `Owned → Exclusive`. 역방향 pass가 계산한 마지막 사용에서 해제하며 임시는 문장 끝에 해제한다. -5. 호출 인자의 `&mut → &`, `[]mut → []`는 Exclusive를 유지하는 임시 공유 view로 검사한다. +4. `&place`: root의 `Owned → Shared(n+1)`. `&mut place`: root의 `Owned → Exclusive`. 역방향 pass가 계산한 마지막 사용에서 해제하며 임시는 문장 끝에 해제한다. 서로 다른 field/index도 같은 root 상태와 충돌한다. +5. 호출 인자의 `&mut → &`, `[]mut → []`만 Exclusive를 유지하는 호출 기간의 임시 shared view로 검사한다. 일반 `let`/대입에는 이 implicit transition을 적용하지 않는다. 6. `Shared`/`Exclusive` 상태에서 금지된 쓰기/이동/재대여를 진단한다(R6). -7. **분기 합류**: `if`/`match`의 각 브랜치를 독립 상태로 계산 후 병합. `Owned` + `Moved` → `MaybeMoved`(사용 에러, drop은 런타임 플래그). -8. **루프**: 본문을 2회 순회. 1회차 종료 상태를 진입 상태와 병합해 2회차 실행, 상태가 수렴하지 않으면 에러. +7. **분기 합류**: `if`/`match`의 각 branch를 독립 상태로 계산한다. branch exit 전에 last-use/liveness에 따라 끝난 borrow를 먼저 해제하고 다음 규칙으로 병합한다. + + | 왼쪽 | 오른쪽 | 결과 | + |---|---|---| + | 같은 상태 | 같은 상태 | 같은 상태 | + | `Owned` | `Moved` | `MaybeMoved` | + | `Moved` | `Owned` | `MaybeMoved` | + | `MaybeMoved` | `Owned`/`Moved`/`MaybeMoved` | `MaybeMoved` | + | `Uninit` | `Owned` 등 initialized 상태 | `MaybeUninit` 또는 동등 상태 | + + merge는 좌우 대칭이다. `MaybeMoved`/`MaybeUninit` 값의 이후 읽기·이동은 에러이고 drop은 필요한 runtime live + flag를 쓴다. 한 경로에서만 borrow가 계속 살아 있으면 합류 뒤에도 살아 있는 것으로 + 보수적으로 취급한다. `Shared(n)`과 `Shared(m)`은 필요한 live shared borrow의 합집합을 + 보존하고 단순 count 구현에서는 적어도 `Shared(max(n,m))`으로 합친다. 한쪽만 + `Exclusive`가 live여도 합류 뒤 root를 `Exclusive` 효과로 잠근다. 서로 양립할 수 없는 + `Shared`/`Exclusive` 상태는 더 약한 상태로 풀지 않고 양쪽 효과를 보존하는 보수적 + 상태로 합치거나 compile error를 낸다. 구현은 state enum/bitset을 확장할 수 있지만 이 + 의미를 만족해야 한다. +8. **루프 fixed point**: loop 진입 상태로 body를 한 번 분석하고 종료/backedge 상태를 + 진입 상태와 위 규칙으로 병합한다. 그 merged 상태로 body를 두 번째 분석한다. v0.1 + compiler A는 이 2-pass를 사용하며 두 번째 분석 뒤에도 의미 상태가 안정되지 않으면 + compile error다. loop 밖에서 생성되어 loop 안에서 이후 사용되는 borrow는 필요하면 + loop 전체에 걸쳐 live로 보고, body에서 생성된 borrow가 backedge를 넘는 경우도 같은 + fixed-point에 포함한다. 첫 iteration만 안전하다는 이유로 허용하지 않는다. 9. R4 위반은 check 단계에서 타입만 보고 거부한다. 단 `static str`은 initializer가 문자열 리터럴인지 함께 확인한다. -10. R8 결과 바인딩을 허용하고 원본 대여를 결과의 마지막 사용까지 전파한다. 메서드는 self 파생, 자유 함수는 유일한 참조성 파라미터 파생인지 본문에서 확인한다. +10. R8 반환 경로마다 `Static`/`Param(N)` provenance를 계산하고 §5 R8 lattice로 합류한다. + 결과 바인딩을 허용하며 `Param(N)` 원본 대여를 결과의 마지막 사용까지 전파한다. + 메서드는 `Param(self)`만, 자유 함수는 유일한 참조성 parameter의 `Param(N)`만 허용하고 + provenance를 lowered signature/`.fei`에 기록한다. 에러 메시지 형식: `file:line:col: error: <설명>` + 관련 위치 `file:line:col: note: <최초 이동/대여 위치>`. @@ -755,7 +1023,7 @@ Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive | M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), handle enum `io.Writer`, 순수 `fmt.fmt_*` | `@print` 오류 삼킴, `@fprint` 전파, `@sprint` 잘림/길이와 인자 타입 진단, safe Writer dangling 불가 | | M5 | `^T`/`^[]T`, drop, defer, 이동·부분 이동 검사 | 누수/이중해제, 소비 close, `mem.replace` 테스트 통과 | | M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | -| M7 | `?T`, `E!T`, try/catch | io 유닛 동작 | +| M7 | `?T`, `E!T`, try/catch | `std.io` 유닛 동작 | | M8 | 유닛/import/.fei, 의존 hash, `fe_errors.h`, 분리 컴파일, std 초안 | 다중 유닛 증분·결정적 빌드 | | M9 | **제네릭** (통합 모노모피제이션) | `fe_generics.c`로 `List(T)`, `Map(K,V)` 중복 없이 빌드 | | M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn, `shared`/`atomic`/`critical`/`interrupt_safe` | QEMU FreeDOS에서 자동화된 far 포인터·인터럽트 공유 상태 테스트 통과(VGA 데모는 수동/멀티모달 검증 대상이라 완료 게이트에서 제외) | @@ -788,8 +1056,36 @@ tests/ - R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `line.trim()` 형태의 슬라이스 반환 연쇄. - `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 `core.Error` 등록, 결정적 `fe_errors.h`, `--strip-error-names`, `fmt.fmt_error`를 각각 pass/fail로 검증한다. +- **M6 필수 edge case**: + - 서로 다른 struct field의 `&mut`라도 같은 root borrow 충돌, 서로 다른 array index도 같은 root borrow 충돌. + - 호출 인자의 `&mut → &`/`[]mut → []` 약화 성공과 일반 `let` binding의 같은 암묵 약화 실패. + - R8 `Static`/`Param(N)` 합류 성공, 서로 다른 `Param` provenance 합류 실패. + - 한 branch에서만 live인 borrow의 보수적 합류, `Owned`/`Moved` 및 초기화 상태 합류. + - loop-carried borrow와 move가 2-pass fixed point에서 안정되는 경우와 불안정해 거부되는 경우. +- **M7 필수 edge case**: + - 문맥 없는 `null` 실패와 parameter/명시 타입으로 결정되는 contextual `null` 성공. + - error declaration의 code 0, 중복 member 이름, 중복 숫자 code 실패. + - `E!T` expected 위치의 success/failure contextual construction과 nominal error 자동 변환 실패. + - `orelse`, 짧은/block `catch`의 lazy side effect·move·borrow. + - non-Copy `Some` pattern이 destructive extraction이 아님을 확인하고 projection 소유 추출에는 `mem.replace`가 필요함을 검증. +- **M8 필수 edge case**: + - dotted unit/import와 alias, 마지막 segment binding, binding conflict. + - unit path/file suffix mismatch, uppercase segment와 8자 초과 segment 거부. + - 서로 다른 root의 동일 canonical unit ambiguity와 같은 canonical file 중복 발견의 dedup. + - reserved `std.*` lookup, 일반 user unit이 builtin std root에서 발견되지 않음. + - private type을 public API에 노출하는 경우 실패와 dotted prefix가 private 권한을 주지 않음. + - private-only non-generic 구현 변경 뒤 dependency interface hash 안정 및 dependent cache hit. + - source/interface graph와 target/model이 같을 때 build order, `-I` order, absolute checkout path가 달라도 byte-identical `.fei`. +- **M9 필수 edge case**: + - value generic과 generic type inference 거부, 명시 type argument 성공. + - alias/underlying type의 instance dedup과 동일 canonical instance body 1회 방출. + - exported generic이 definition-unit private helper/private generic을 호출하는 support closure. + - 같은 instance recursion의 pending 재사용과 distinct growing-instance chain depth 32 초과 실패. + - generic request 발견 순서와 build order가 달라도 byte-identical `fe_generics.c`. + - invalid dependent operation의 definition 위치 primary error와 `instantiated here` chain note. - 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. -- 회귀 실행: `make test` — 전 타깃 전 테스트. +- 정식 회귀 실행은 QEMU FreeDOS 내부 Open Watcom의 `TEST-DOS.BAT`로 전 타깃·마일스톤 + gate를 확인한다. host compiler 결과는 편집 보조일 뿐 완료 판정에 사용하지 않는다. --- @@ -839,6 +1135,6 @@ dump(&mut file, buf); // &mut File → &mut dyn Writer 자동 변환 - **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다. - `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱). - `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립 마일스톤으로 다룬다. -- 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1.2의 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다. +- 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다. 위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토. From 5981d2b5b12964883ca260fd5dea8265d9c03639 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:56:15 +0900 Subject: [PATCH 050/184] add M6 ownership core interfaces --- fec/src/own.h | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/fec/src/own.h b/fec/src/own.h index eb0ff65..830341f 100644 --- a/fec/src/own.h +++ b/fec/src/own.h @@ -13,7 +13,80 @@ enum FeOwnMoveState { FE_OWN_MAYBE_MOVED = 2 }; +typedef enum FeOwnAccessKind { + FE_OWN_READ, + FE_OWN_WRITE, + FE_OWN_MOVE, + FE_OWN_BORROW_SHARED, + FE_OWN_BORROW_MUT, + FE_OWN_PROJECTION +} FeOwnAccessKind; + +typedef enum FeOwnProvenanceKind { + FE_OWN_PROV_INVALID, + FE_OWN_PROV_STATIC, + FE_OWN_PROV_PARAM +} FeOwnProvenanceKind; + +typedef struct FeOwnPlace { + FeNode *root; + const char *root_cname; + int projected; +} FeOwnPlace; + +typedef struct FeOwnState { + int move; + int initialized; + unsigned shared; + int exclusive; + int borrow_conflict; + FeLoc move_loc; + FeLoc borrow_loc; +} FeOwnState; + +typedef struct FeOwnProvenance { + FeOwnProvenanceKind kind; + unsigned param_index; +} FeOwnProvenance; + +typedef struct FeOwnLastUse { + const char *cname; + FeNode *decl; + FeNode *last_node; + unsigned long last_ordinal; + int defer_extended; +} FeOwnLastUse; + +typedef struct FeOwnLiveness { + FeArena *arena; + FeOwnLastUse *items; + unsigned count; + unsigned capacity; + unsigned long ordinal; +} FeOwnLiveness; + int fe_own_is_copy_type(FeType *type); +int fe_own_is_reference_like(FeType *type); +int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place); + +void fe_own_state_init(FeOwnState *state, int initialized); +int fe_own_access(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); +void fe_own_release_shared(FeOwnState *state); +void fe_own_release_exclusive(FeOwnState *state); +FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right); +int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right); + +FeOwnProvenance fe_own_provenance_static(void); +FeOwnProvenance fe_own_provenance_param(unsigned param_index); +FeOwnProvenance fe_own_merge_provenance(FeOwnProvenance left, + FeOwnProvenance right); + +void fe_own_liveness_init(FeOwnLiveness *live, FeArena *arena); +int fe_own_collect_last_uses(FeOwnLiveness *live, FeNode *fn); +const FeOwnLastUse *fe_own_last_use(const FeOwnLiveness *live, + const char *cname); + void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, FeNode *expr, FeType *type, int in_defer); void fe_own_check_use(FeDiags *diags, int state, FeLoc loc); From db2b681f6b4ede22426f724de09afe91206fb57b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:57:15 +0900 Subject: [PATCH 051/184] implement M6 ownership core --- fec/src/own.c | 391 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 391 insertions(+) diff --git a/fec/src/own.c b/fec/src/own.c index 0d90817..862d330 100644 --- a/fec/src/own.c +++ b/fec/src/own.c @@ -1,4 +1,21 @@ #include "own.h" +#include + +static FeLoc fe_own_no_loc(void) +{ + FeLoc loc; + loc.file = 0; + loc.line = 0; + loc.col = 0; + return loc; +} + +static void fe_own_error_note(FeDiags *diags, FeLoc loc, const char *msg, + FeLoc note, const char *note_msg) +{ + fe_diag_error(diags, loc, msg); + if (note.file) fe_diag_note_src(diags, note, note_msg); +} int fe_own_is_copy_type(FeType *type) { @@ -25,6 +42,380 @@ int fe_own_is_copy_type(FeType *type) return 1; } +int fe_own_is_reference_like(FeType *type) +{ + return type && (type->kind == FE_TYPE_REF || + type->kind == FE_TYPE_SLICE || + type->kind == FE_TYPE_STR); +} + +static FeNode *fe_own_root_expr(FeNode *expr) +{ + if (!expr) return 0; + if (expr->kind == FE_N_IDENT) return expr; + if (expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) + return fe_own_root_expr(expr->a); + if (expr->kind == FE_N_UNARY && expr->text && + (strcmp(expr->text, "&") == 0 || strcmp(expr->text, "&mut") == 0)) + return fe_own_root_expr(expr->a); + return 0; +} + +int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place) +{ + FeNode *root; + if (!place) return 0; + place->root = 0; + place->root_cname = 0; + place->projected = 0; + root = fe_own_root_expr(expr); + if (!root) return 0; + place->root = root; + place->root_cname = root->cname ? root->cname : root->text; + place->projected = root != expr; + return place->root_cname != 0; +} + +void fe_own_state_init(FeOwnState *state, int initialized) +{ + if (!state) return; + state->move = FE_OWN_AVAILABLE; + state->initialized = initialized != 0; + state->shared = 0; + state->exclusive = 0; + state->borrow_conflict = 0; + state->move_loc = fe_own_no_loc(); + state->borrow_loc = fe_own_no_loc(); +} + +static int fe_own_require_value(FeDiags *diags, FeOwnState *state, FeLoc loc) +{ + if (!state->initialized) { + fe_diag_error(diags, loc, "use of uninitialized variable"); + return 0; + } + if (state->move == FE_OWN_MOVED) { + fe_own_error_note(diags, loc, "use of moved value", state->move_loc, + "value was moved here"); + return 0; + } + if (state->move == FE_OWN_MAYBE_MOVED) { + fe_diag_error(diags, loc, "use of possibly moved value"); + return 0; + } + return 1; +} + +static int fe_own_require_stable_borrow(FeDiags *diags, FeOwnState *state, + FeLoc loc) +{ + if (!state->borrow_conflict) return 1; + fe_own_error_note(diags, loc, + "incompatible borrow state across control-flow paths", + state->borrow_loc, "borrow originated here"); + return 0; +} + +int fe_own_access(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) +{ + if (!state) return 0; + if (access == FE_OWN_PROJECTION) return 1; + + if (!fe_own_require_stable_borrow(diags, state, loc)) return 0; + + if (access == FE_OWN_WRITE) { + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, "cannot write while value is borrowed", + state->borrow_loc, "borrow originated here"); + return 0; + } + state->move = FE_OWN_AVAILABLE; + state->initialized = 1; + state->move_loc = fe_own_no_loc(); + return 1; + } + + if (!fe_own_require_value(diags, state, loc)) return 0; + + switch (access) { + case FE_OWN_READ: + if (state->exclusive) { + fe_own_error_note(diags, loc, + "cannot read directly while value is mutably borrowed", + state->borrow_loc, "mutable borrow originated here"); + return 0; + } + return 1; + case FE_OWN_MOVE: + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, "cannot move while value is borrowed", + state->borrow_loc, "borrow originated here"); + return 0; + } + state->move = FE_OWN_MOVED; + state->initialized = 0; + state->move_loc = loc; + return 1; + case FE_OWN_BORROW_SHARED: + if (state->exclusive) { + fe_own_error_note(diags, loc, + "cannot create shared borrow while mutable borrow is live", + state->borrow_loc, "mutable borrow originated here"); + return 0; + } + if (!state->shared) state->borrow_loc = loc; + ++state->shared; + return 1; + case FE_OWN_BORROW_MUT: + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, + "cannot create mutable borrow while another borrow is live", + state->borrow_loc, "existing borrow originated here"); + return 0; + } + state->exclusive = 1; + state->borrow_loc = loc; + return 1; + default: + break; + } + return 1; +} + +void fe_own_release_shared(FeOwnState *state) +{ + if (!state || !state->shared) return; + --state->shared; + if (!state->shared && !state->exclusive) + state->borrow_loc = fe_own_no_loc(); +} + +void fe_own_release_exclusive(FeOwnState *state) +{ + if (!state) return; + state->exclusive = 0; + if (!state->shared) state->borrow_loc = fe_own_no_loc(); +} + +FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right) +{ + FeOwnState out; + out.move = left.move == right.move ? left.move : + fe_own_merge_move(left.move, right.move); + out.initialized = left.initialized && right.initialized; + out.shared = left.shared > right.shared ? left.shared : right.shared; + out.exclusive = left.exclusive || right.exclusive; + out.borrow_conflict = left.borrow_conflict || right.borrow_conflict || + (out.shared != 0 && out.exclusive != 0); + out.move_loc = left.move != FE_OWN_AVAILABLE ? left.move_loc : right.move_loc; + if (left.shared || left.exclusive || left.borrow_conflict) + out.borrow_loc = left.borrow_loc; + else + out.borrow_loc = right.borrow_loc; + return out; +} + +int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right) +{ + if (!left || !right) return 0; + return left->move == right->move && + left->initialized == right->initialized && + left->shared == right->shared && + left->exclusive == right->exclusive && + left->borrow_conflict == right->borrow_conflict; +} + +FeOwnProvenance fe_own_provenance_static(void) +{ + FeOwnProvenance p; + p.kind = FE_OWN_PROV_STATIC; + p.param_index = 0; + return p; +} + +FeOwnProvenance fe_own_provenance_param(unsigned param_index) +{ + FeOwnProvenance p; + p.kind = FE_OWN_PROV_PARAM; + p.param_index = param_index; + return p; +} + +FeOwnProvenance fe_own_merge_provenance(FeOwnProvenance left, + FeOwnProvenance right) +{ + FeOwnProvenance invalid; + invalid.kind = FE_OWN_PROV_INVALID; + invalid.param_index = 0; + if (left.kind == FE_OWN_PROV_INVALID || right.kind == FE_OWN_PROV_INVALID) + return invalid; + if (left.kind == FE_OWN_PROV_STATIC) return right; + if (right.kind == FE_OWN_PROV_STATIC) return left; + if (left.param_index == right.param_index) return left; + return invalid; +} + +void fe_own_liveness_init(FeOwnLiveness *live, FeArena *arena) +{ + if (!live) return; + live->arena = arena; + live->items = 0; + live->count = 0; + live->capacity = 0; + live->ordinal = 0; +} + +static FeOwnLastUse *fe_own_live_find(FeOwnLiveness *live, const char *cname) +{ + unsigned i; + if (!live || !cname) return 0; + for (i = 0; i < live->count; ++i) + if (live->items[i].cname == cname || + strcmp(live->items[i].cname, cname) == 0) + return &live->items[i]; + return 0; +} + +static int fe_own_live_add(FeOwnLiveness *live, FeNode *decl) +{ + FeOwnLastUse *items; + FeOwnLastUse *slot; + unsigned capacity; + const char *cname; + if (!live || !decl || !live->arena || !fe_own_is_reference_like(decl->sem_type)) + return 1; + cname = decl->cname ? decl->cname : decl->text; + if (!cname || fe_own_live_find(live, cname)) return 1; + if (live->count == live->capacity) { + capacity = live->capacity ? live->capacity * 2U : 8U; + items = (FeOwnLastUse *)fe_arena_alloc(live->arena, + capacity * sizeof(FeOwnLastUse)); + if (!items) return 0; + if (live->items) + memcpy(items, live->items, live->count * sizeof(FeOwnLastUse)); + live->items = items; + live->capacity = capacity; + } + slot = &live->items[live->count++]; + slot->cname = cname; + slot->decl = decl; + slot->last_node = 0; + slot->last_ordinal = 0; + slot->defer_extended = 0; + return 1; +} + +static unsigned long fe_own_node_weight(FeNode *node); + +static unsigned long fe_own_list_weight(FeNode *node) +{ + unsigned long count; + count = 0; + while (node) { + count += fe_own_node_weight(node); + node = node->next; + } + return count; +} + +static unsigned long fe_own_node_weight(FeNode *node) +{ + unsigned long count; + if (!node) return 0; + count = 1; + count += fe_own_node_weight(node->a); + count += fe_own_node_weight(node->b); + count += fe_own_node_weight(node->c); + count += fe_own_list_weight(node->children); + return count; +} + +static int fe_own_live_visit(FeOwnLiveness *live, FeNode *node, + unsigned long block_end, + unsigned long defer_until); + +static int fe_own_live_visit_list(FeOwnLiveness *live, FeNode *node, + unsigned long block_end, + unsigned long defer_until) +{ + while (node) { + if (!fe_own_live_visit(live, node, block_end, defer_until)) return 0; + node = node->next; + } + return 1; +} + +static int fe_own_live_visit(FeOwnLiveness *live, FeNode *node, + unsigned long block_end, + unsigned long defer_until) +{ + FeOwnLastUse *slot; + unsigned long end; + unsigned long effective; + if (!node) return 1; + ++live->ordinal; + + if (node->kind == FE_N_IDENT) { + slot = fe_own_live_find(live, node->cname ? node->cname : node->text); + if (slot) { + effective = defer_until ? defer_until : live->ordinal; + if (effective >= slot->last_ordinal) { + slot->last_ordinal = effective; + slot->last_node = node; + if (defer_until) slot->defer_extended = 1; + } + } + return 1; + } + + if (node->kind == FE_N_BLOCK) { + end = live->ordinal + fe_own_list_weight(node->children); + return fe_own_live_visit_list(live, node->children, end, defer_until); + } + + if (node->kind == FE_N_DEFER) { + effective = defer_until ? defer_until : block_end; + return fe_own_live_visit(live, node->a, block_end, effective); + } + + if (!fe_own_live_visit(live, node->a, block_end, defer_until)) return 0; + if (!fe_own_live_visit(live, node->b, block_end, defer_until)) return 0; + if (!fe_own_live_visit(live, node->c, block_end, defer_until)) return 0; + if (!fe_own_live_visit_list(live, node->children, block_end, defer_until)) + return 0; + + if (node->kind == FE_N_LET || node->kind == FE_N_VAR || + node->kind == FE_N_CONST) + return fe_own_live_add(live, node); + return 1; +} + +int fe_own_collect_last_uses(FeOwnLiveness *live, FeNode *fn) +{ + FeNode *param; + if (!live || !fn || fn->kind != FE_N_FN) return 0; + live->items = 0; + live->count = 0; + live->capacity = 0; + live->ordinal = 0; + for (param = fn->a ? fn->a->children : 0; param; param = param->next) + if (!fe_own_live_add(live, param)) return 0; + return fe_own_live_visit(live, fn->c, 0, 0); +} + +const FeOwnLastUse *fe_own_last_use(const FeOwnLiveness *live, + const char *cname) +{ + unsigned i; + if (!live || !cname) return 0; + for (i = 0; i < live->count; ++i) + if (live->items[i].cname == cname || + strcmp(live->items[i].cname, cname) == 0) + return &live->items[i]; + return 0; +} + void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, FeNode *expr, FeType *type, int in_defer) { From 8c4ec8a6e65bfefd2237eddb5a3e8f03d9168ac2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:58:18 +0900 Subject: [PATCH 052/184] complete M6 ownership core primitives --- fec/src/own.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fec/src/own.h b/fec/src/own.h index 830341f..927ff97 100644 --- a/fec/src/own.h +++ b/fec/src/own.h @@ -72,10 +72,13 @@ int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place); void fe_own_state_init(FeOwnState *state, int initialized); int fe_own_access(FeDiags *diags, FeOwnState *state, FeOwnAccessKind access, FeLoc loc); +int fe_own_call_shared_view(FeDiags *diags, FeOwnState *state, FeLoc loc); void fe_own_release_shared(FeOwnState *state); void fe_own_release_exclusive(FeOwnState *state); FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right); int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right); +int fe_own_loop_merge_state(FeOwnState entry, FeOwnState backedge, + FeOwnState *merged); FeOwnProvenance fe_own_provenance_static(void); FeOwnProvenance fe_own_provenance_param(unsigned param_index); From 3bcf6a33f184e2137d7e4f04be95baaaad23c3bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 19:59:01 +0900 Subject: [PATCH 053/184] tighten M6 ownership core semantics --- fec/src/own.c | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/fec/src/own.c b/fec/src/own.c index 862d330..f40fd43 100644 --- a/fec/src/own.c +++ b/fec/src/own.c @@ -61,6 +61,16 @@ static FeNode *fe_own_root_expr(FeNode *expr) return 0; } +static int fe_own_expr_has_projection(FeNode *expr) +{ + if (!expr) return 0; + if (expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) return 1; + if (expr->kind == FE_N_UNARY && expr->text && + (strcmp(expr->text, "&") == 0 || strcmp(expr->text, "&mut") == 0)) + return fe_own_expr_has_projection(expr->a); + return 0; +} + int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place) { FeNode *root; @@ -72,7 +82,7 @@ int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place) if (!root) return 0; place->root = root; place->root_cname = root->cname ? root->cname : root->text; - place->projected = root != expr; + place->projected = fe_own_expr_has_projection(expr); return place->root_cname != 0; } @@ -90,10 +100,6 @@ void fe_own_state_init(FeOwnState *state, int initialized) static int fe_own_require_value(FeDiags *diags, FeOwnState *state, FeLoc loc) { - if (!state->initialized) { - fe_diag_error(diags, loc, "use of uninitialized variable"); - return 0; - } if (state->move == FE_OWN_MOVED) { fe_own_error_note(diags, loc, "use of moved value", state->move_loc, "value was moved here"); @@ -103,6 +109,10 @@ static int fe_own_require_value(FeDiags *diags, FeOwnState *state, FeLoc loc) fe_diag_error(diags, loc, "use of possibly moved value"); return 0; } + if (!state->initialized) { + fe_diag_error(diags, loc, "use of uninitialized variable"); + return 0; + } return 1; } @@ -183,6 +193,19 @@ int fe_own_access(FeDiags *diags, FeOwnState *state, return 1; } +int fe_own_call_shared_view(FeDiags *diags, FeOwnState *state, FeLoc loc) +{ + if (!state) return 0; + if (!fe_own_require_stable_borrow(diags, state, loc)) return 0; + if (!fe_own_require_value(diags, state, loc)) return 0; + if (!state->exclusive) { + fe_diag_error(diags, loc, + "read-only reborrow requires a live mutable borrow"); + return 0; + } + return 1; +} + void fe_own_release_shared(FeOwnState *state) { if (!state || !state->shared) return; @@ -226,6 +249,14 @@ int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right) left->borrow_conflict == right->borrow_conflict; } +int fe_own_loop_merge_state(FeOwnState entry, FeOwnState backedge, + FeOwnState *merged) +{ + if (!merged) return 0; + *merged = fe_own_merge_state(entry, backedge); + return fe_own_state_equal(&entry, merged); +} + FeOwnProvenance fe_own_provenance_static(void) { FeOwnProvenance p; From 748505513dea59e157a0422de0d79c39186106ed Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 19:56:24 +0900 Subject: [PATCH 054/184] test: audit M6-M9 fixtures for v0.1.8 --- fec/tests/m6/README.md | 2 +- fec/tests/m6/badbinit.fe | 10 ++++++++++ fec/tests/m6/badbrmov.fe | 9 +++++++++ fec/tests/m6/badloop.fe | 9 +++++++++ fec/tests/m6/badrfld.fe | 11 +++++++++++ fec/tests/m6/badridx.fe | 10 ++++++++++ fec/tests/m6/badweak.fe | 9 +++++++++ fec/tests/m6/okr8join.fe | 14 ++++++++++++++ fec/tests/m6/okrtlast.fe | 11 +++++++++++ fec/tests/m6/okwcall.fe | 11 +++++++++++ fec/tests/m7/README.md | 2 +- fec/tests/m7/badercod.fe | 7 +++++++ fec/tests/m7/badernam.fe | 7 +++++++ fec/tests/m7/badnull.fe | 6 ++++++ fec/tests/m7/okcatmov.fe | 8 ++++++++ fec/tests/m7/oknull.fe | 10 ++++++++++ fec/tests/m7/okpatvw.fe | 12 ++++++++++++ fec/tests/m8/README.md | 7 +++++++ fec/tests/m8/alias/acme/math.fe | 3 +++ fec/tests/m8/alias/main.fe | 4 ++++ fec/tests/m8/badlong/main.fe | 4 ++++ fec/tests/m8/badupper/main.fe | 4 ++++ fec/tests/m8/bindconf/alpha/net.fe | 1 + fec/tests/m8/bindconf/beta/net.fe | 1 + fec/tests/m8/bindconf/main.fe | 6 ++++++ fec/tests/m8/dotpriv/game/bar.fe | 5 +++++ fec/tests/m8/dotpriv/game/foo.fe | 3 +++ fec/tests/m8/dotpriv/main.fe | 4 ++++ fec/tests/m8/dotted/acme/math.fe | 3 +++ fec/tests/m8/dotted/main.fe | 4 ++++ fec/tests/m8/pubpriv/lib.fe | 5 +++++ fec/tests/m8/pubpriv/main.fe | 4 ++++ fec/tests/m9/README.md | 4 +++- fec/tests/m9/baddist.fe | 10 ++++++++++ fec/tests/m9/badinfer.fe | 8 ++++++++ fec/tests/m9/badvalue.fe | 4 ++++ fec/tests/m9/oksamrec.fe | 7 +++++++ fec/tests/m9/{badscope => okscope}/lib.fe | 1 - fec/tests/m9/{badscope => okscope}/main.fe | 0 fec/tests/m9/okskip.fe | 11 +++++++++++ 40 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 fec/tests/m6/badbinit.fe create mode 100644 fec/tests/m6/badbrmov.fe create mode 100644 fec/tests/m6/badloop.fe create mode 100644 fec/tests/m6/badrfld.fe create mode 100644 fec/tests/m6/badridx.fe create mode 100644 fec/tests/m6/badweak.fe create mode 100644 fec/tests/m6/okr8join.fe create mode 100644 fec/tests/m6/okrtlast.fe create mode 100644 fec/tests/m6/okwcall.fe create mode 100644 fec/tests/m7/badercod.fe create mode 100644 fec/tests/m7/badernam.fe create mode 100644 fec/tests/m7/badnull.fe create mode 100644 fec/tests/m7/okcatmov.fe create mode 100644 fec/tests/m7/oknull.fe create mode 100644 fec/tests/m7/okpatvw.fe create mode 100644 fec/tests/m8/alias/acme/math.fe create mode 100644 fec/tests/m8/alias/main.fe create mode 100644 fec/tests/m8/badlong/main.fe create mode 100644 fec/tests/m8/badupper/main.fe create mode 100644 fec/tests/m8/bindconf/alpha/net.fe create mode 100644 fec/tests/m8/bindconf/beta/net.fe create mode 100644 fec/tests/m8/bindconf/main.fe create mode 100644 fec/tests/m8/dotpriv/game/bar.fe create mode 100644 fec/tests/m8/dotpriv/game/foo.fe create mode 100644 fec/tests/m8/dotpriv/main.fe create mode 100644 fec/tests/m8/dotted/acme/math.fe create mode 100644 fec/tests/m8/dotted/main.fe create mode 100644 fec/tests/m8/pubpriv/lib.fe create mode 100644 fec/tests/m8/pubpriv/main.fe create mode 100644 fec/tests/m9/baddist.fe create mode 100644 fec/tests/m9/badinfer.fe create mode 100644 fec/tests/m9/badvalue.fe create mode 100644 fec/tests/m9/oksamrec.fe rename fec/tests/m9/{badscope => okscope}/lib.fe (81%) rename fec/tests/m9/{badscope => okscope}/main.fe (100%) create mode 100644 fec/tests/m9/okskip.fe diff --git a/fec/tests/m6/README.md b/fec/tests/m6/README.md index 9e1e0ef..743005f 100644 --- a/fec/tests/m6/README.md +++ b/fec/tests/m6/README.md @@ -8,4 +8,4 @@ These fixtures pin the M6 ownership/borrow rules before implementation. - When M6 starts, wire this directory into the DOS/QEMU gate without changing the expected result of any fixture. - M6 also owns the general-global borrow restriction from R10 because AGENTS.md explicitly groups that change with the `own.c` state-machine work. -Coverage: R4 storage restrictions, R5 scope, R6 shared/exclusive liveness and reborrows, R7 invalidation, R8 derived returns, defer lifetime extension, and global borrow restrictions. +Coverage: R4 storage restrictions, R5 scope, R6 shared/exclusive liveness, root-local field/index conflicts, call-only `&mut -> &` reborrows, branch/loop `MaybeMoved` and initialization-state merging, R7 invalidation, R8 derived-return provenance joins, defer lifetime extension, and global borrow restrictions. diff --git a/fec/tests/m6/badbinit.fe b/fec/tests/m6/badbinit.fe new file mode 100644 index 0000000..b6eb632 --- /dev/null +++ b/fec/tests/m6/badbinit.fe @@ -0,0 +1,10 @@ +// ERROR:9:initialized +unit badbinit; + +fn bad(assign: bool) -> i32 { + var value: i32; + if assign { + value = 1; + } + return value; +} diff --git a/fec/tests/m6/badbrmov.fe b/fec/tests/m6/badbrmov.fe new file mode 100644 index 0000000..7dbb11b --- /dev/null +++ b/fec/tests/m6/badbrmov.fe @@ -0,0 +1,9 @@ +// ERROR:9:move +unit badbrmov; + +fn bad(p: ^i32, consume: bool) -> void { + if consume { + mem.destroy(p); + } + mem.destroy(p); +} diff --git a/fec/tests/m6/badloop.fe b/fec/tests/m6/badloop.fe new file mode 100644 index 0000000..fed8c0f --- /dev/null +++ b/fec/tests/m6/badloop.fe @@ -0,0 +1,9 @@ +// ERROR:8:move +unit badloop; + +fn bad(p: ^i32, again: bool) -> void { + while again { + mem.destroy(p); + } + mem.destroy(p); +} diff --git a/fec/tests/m6/badrfld.fe b/fec/tests/m6/badrfld.fe new file mode 100644 index 0000000..65cb8b0 --- /dev/null +++ b/fec/tests/m6/badrfld.fe @@ -0,0 +1,11 @@ +// ERROR:9:borrow +unit badrfld; + +struct Pair { a: i32, b: i32, } + +fn bad() -> void { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + p.b = 3; + left.^ = 4; +} diff --git a/fec/tests/m6/badridx.fe b/fec/tests/m6/badridx.fe new file mode 100644 index 0000000..d7597c5 --- /dev/null +++ b/fec/tests/m6/badridx.fe @@ -0,0 +1,10 @@ +// ERROR:7:borrow +unit badridx; + +fn bad() -> void { + var xs: [2]i32 = [1, 2]; + let a = &mut xs[0]; + let b = &mut xs[1]; + a.^ = 3; + b.^ = 4; +} diff --git a/fec/tests/m6/badweak.fe b/fec/tests/m6/badweak.fe new file mode 100644 index 0000000..8da6bc8 --- /dev/null +++ b/fec/tests/m6/badweak.fe @@ -0,0 +1,9 @@ +// ERROR:7:mut +unit badweak; + +fn bad() -> void { + var x: i32 = 0; + let m = &mut x; + let s: &i32 = m; + let v = s.^; +} diff --git a/fec/tests/m6/okr8join.fe b/fec/tests/m6/okr8join.fe new file mode 100644 index 0000000..b2e67dd --- /dev/null +++ b/fec/tests/m6/okr8join.fe @@ -0,0 +1,14 @@ +unit okr8join; + +fn select(s: str, from_param: bool) -> str { + if from_param { return s; } + return "static"; +} + +fn test() -> u8 { + var bytes: [1]u8 = [7 as u8]; + let view = select(bytes[..], true); + let value = view[0]; + bytes[0] = 8 as u8; + return value; +} diff --git a/fec/tests/m6/okrtlast.fe b/fec/tests/m6/okrtlast.fe new file mode 100644 index 0000000..bd87c4e --- /dev/null +++ b/fec/tests/m6/okrtlast.fe @@ -0,0 +1,11 @@ +unit okrtlast; + +struct Pair { a: i32, b: i32, } + +fn test() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + left.^ = 3; + p.b = 4; + return p.a + p.b; +} diff --git a/fec/tests/m6/okwcall.fe b/fec/tests/m6/okwcall.fe new file mode 100644 index 0000000..a53efe2 --- /dev/null +++ b/fec/tests/m6/okwcall.fe @@ -0,0 +1,11 @@ +unit okwcall; + +fn read(r: &i32) -> i32 { return r.^; } + +fn test() -> i32 { + var x: i32 = 5; + let m = &mut x; + let value = read(m); + m.^ = value + 1; + return m.^; +} diff --git a/fec/tests/m7/README.md b/fec/tests/m7/README.md index 468c98b..3679b4b 100644 --- a/fec/tests/m7/README.md +++ b/fec/tests/m7/README.md @@ -5,4 +5,4 @@ M7 adds optionals and error unions on top of the M6 ownership model. `ok*.fe` must compile. `bad*.fe` must fail according to the first-line error marker. The files are not wired into `TEST-DOS.BAT` until M7 work begins. -Coverage: `?T`, `.?`, `Some`/`None` pattern-only destructuring, `mem.replace` extraction, `orelse`, nominal error unions, `try`, block/short `catch`, error code zero rejection, and R4/R7 interactions with optional references/owners. +Coverage: contextual `null`, `?T`, `.?`, `Some`/`None` pattern-only non-destructive views, `mem.replace` extraction, lazy `orelse`/`catch`, nominal error unions, `try`, block/short `catch`, and error code/name uniqueness, plus R4/R7 interactions with optional references/owners. diff --git a/fec/tests/m7/badercod.fe b/fec/tests/m7/badercod.fe new file mode 100644 index 0000000..ad5b34c --- /dev/null +++ b/fec/tests/m7/badercod.fe @@ -0,0 +1,7 @@ +// ERROR:6:duplicate +unit badercod; + +error E { + One = 1, + Two = 1, +} diff --git a/fec/tests/m7/badernam.fe b/fec/tests/m7/badernam.fe new file mode 100644 index 0000000..661e8c0 --- /dev/null +++ b/fec/tests/m7/badernam.fe @@ -0,0 +1,7 @@ +// ERROR:6:duplicate +unit badernam; + +error E { + One = 1, + One = 2, +} diff --git a/fec/tests/m7/badnull.fe b/fec/tests/m7/badnull.fe new file mode 100644 index 0000000..4ed896b --- /dev/null +++ b/fec/tests/m7/badnull.fe @@ -0,0 +1,6 @@ +// ERROR:5:null +unit badnull; + +fn bad() -> void { + let p = null; +} diff --git a/fec/tests/m7/okcatmov.fe b/fec/tests/m7/okcatmov.fe new file mode 100644 index 0000000..c2c7b37 --- /dev/null +++ b/fec/tests/m7/okcatmov.fe @@ -0,0 +1,8 @@ +unit okcatmov; + +error E { Bad = 1, } +struct Node { value: i32, } + +fn recover(result: E!^Node, fallback: ^Node) -> ^Node { + return result catch fallback; +} diff --git a/fec/tests/m7/oknull.fe b/fec/tests/m7/oknull.fe new file mode 100644 index 0000000..c12a917 --- /dev/null +++ b/fec/tests/m7/oknull.fe @@ -0,0 +1,10 @@ +unit oknull; + +struct Node { value: i32, } + +fn accepts(p: ?^Node) -> bool { return p == null; } + +fn test() -> bool { + let p: ?^Node = null; + return accepts(p); +} diff --git a/fec/tests/m7/okpatvw.fe b/fec/tests/m7/okpatvw.fe new file mode 100644 index 0000000..5797b46 --- /dev/null +++ b/fec/tests/m7/okpatvw.fe @@ -0,0 +1,12 @@ +unit okpatvw; + +struct Node { value: i32, } + +fn keep(p: ?^Node) -> void { + var slot = p; + if let Some(node) = slot { + let value = node.value; + } + let owned = mem.replace(&mut slot, null).?; + mem.destroy(owned); +} diff --git a/fec/tests/m8/README.md b/fec/tests/m8/README.md index 73e69e4..6271483 100644 --- a/fec/tests/m8/README.md +++ b/fec/tests/m8/README.md @@ -5,6 +5,8 @@ M8 is the first multi-unit milestone, so cases live in subdirectories. Each case Pass cases: - `basic`: public function across units. - `pubfld`: public type and public field across units. +- `dotted` and `alias`: canonical dotted unit paths, last-segment binding, and explicit import aliases. +- `dotpriv`: a dotted unit prefix does not grant access to another unit's private declarations. - `errsame`: two units use the same anonymous `error.Name`; the driver must assign one deterministic `core.Error` code. - `errdet`: the same anonymous error-name set appears in a different source/import order; generated `fe_errors.h` must be byte-identical to `errsame` modulo the intentionally different unit graph. @@ -14,6 +16,11 @@ Fail cases: - `missing`: unresolved import. - `cycle`: cyclic imports. - `unitbad`: filename/unit-name mismatch. +- `badupper` and `badlong`: DOS-safe lowercase, eight-character unit-segment limit. +- `bindconf`: two imports with the same last-segment binding require an alias. +- `pubpriv`: a public signature cannot expose a private nominal type. - `errnom`: nominal error cannot flow into `core.Error` via `try`. The current M5 `TEST-DOS.BAT` is intentionally unchanged. M8 should add procedural checks for `.fei` creation/hash invalidation and deterministic `fe_errors.h` using these fixtures. + +The M8 DOS gate must additionally construct two separate `-I` roots containing the same canonical unit and require an ambiguity error; repeat the case with two paths to the same canonical file and require deduplication. It must also verify that `std.*` resolves only from the built-in std root, ordinary user units never do, and that changing a private non-generic implementation preserves the dependent interface-cache hit. Those checks need temporary roots/cache inspection and deliberately remain procedural rather than encoding host paths in fixtures. diff --git a/fec/tests/m8/alias/acme/math.fe b/fec/tests/m8/alias/acme/math.fe new file mode 100644 index 0000000..ee6d1e8 --- /dev/null +++ b/fec/tests/m8/alias/acme/math.fe @@ -0,0 +1,3 @@ +unit acme.math; + +pub fn answer() -> i32 { return 42; } diff --git a/fec/tests/m8/alias/main.fe b/fec/tests/m8/alias/main.fe new file mode 100644 index 0000000..7e189d2 --- /dev/null +++ b/fec/tests/m8/alias/main.fe @@ -0,0 +1,4 @@ +unit main; +import acme.math as calc; + +fn main() -> i32 { return calc.answer(); } diff --git a/fec/tests/m8/badlong/main.fe b/fec/tests/m8/badlong/main.fe new file mode 100644 index 0000000..ae41a21 --- /dev/null +++ b/fec/tests/m8/badlong/main.fe @@ -0,0 +1,4 @@ +// ERROR:2:unit +unit toolonggg; + +fn main() -> void {} diff --git a/fec/tests/m8/badupper/main.fe b/fec/tests/m8/badupper/main.fe new file mode 100644 index 0000000..7ae7913 --- /dev/null +++ b/fec/tests/m8/badupper/main.fe @@ -0,0 +1,4 @@ +// ERROR:2:unit +unit Upper; + +fn main() -> void {} diff --git a/fec/tests/m8/bindconf/alpha/net.fe b/fec/tests/m8/bindconf/alpha/net.fe new file mode 100644 index 0000000..02af990 --- /dev/null +++ b/fec/tests/m8/bindconf/alpha/net.fe @@ -0,0 +1 @@ +unit alpha.net; diff --git a/fec/tests/m8/bindconf/beta/net.fe b/fec/tests/m8/bindconf/beta/net.fe new file mode 100644 index 0000000..1c5f6f1 --- /dev/null +++ b/fec/tests/m8/bindconf/beta/net.fe @@ -0,0 +1 @@ +unit beta.net; diff --git a/fec/tests/m8/bindconf/main.fe b/fec/tests/m8/bindconf/main.fe new file mode 100644 index 0000000..6b1a90d --- /dev/null +++ b/fec/tests/m8/bindconf/main.fe @@ -0,0 +1,6 @@ +// ERROR:4:binding +unit main; +import alpha.net; +import beta.net; + +fn main() -> void {} diff --git a/fec/tests/m8/dotpriv/game/bar.fe b/fec/tests/m8/dotpriv/game/bar.fe new file mode 100644 index 0000000..3dba2e9 --- /dev/null +++ b/fec/tests/m8/dotpriv/game/bar.fe @@ -0,0 +1,5 @@ +// ERROR:5:private +unit game.bar; +import game.foo; + +pub fn read() -> i32 { return foo.hidden(); } diff --git a/fec/tests/m8/dotpriv/game/foo.fe b/fec/tests/m8/dotpriv/game/foo.fe new file mode 100644 index 0000000..ef2220e --- /dev/null +++ b/fec/tests/m8/dotpriv/game/foo.fe @@ -0,0 +1,3 @@ +unit game.foo; + +fn hidden() -> i32 { return 1; } diff --git a/fec/tests/m8/dotpriv/main.fe b/fec/tests/m8/dotpriv/main.fe new file mode 100644 index 0000000..b14ac8d --- /dev/null +++ b/fec/tests/m8/dotpriv/main.fe @@ -0,0 +1,4 @@ +unit main; +import game.bar; + +fn main() -> i32 { return bar.read(); } diff --git a/fec/tests/m8/dotted/acme/math.fe b/fec/tests/m8/dotted/acme/math.fe new file mode 100644 index 0000000..ee6d1e8 --- /dev/null +++ b/fec/tests/m8/dotted/acme/math.fe @@ -0,0 +1,3 @@ +unit acme.math; + +pub fn answer() -> i32 { return 42; } diff --git a/fec/tests/m8/dotted/main.fe b/fec/tests/m8/dotted/main.fe new file mode 100644 index 0000000..df3abae --- /dev/null +++ b/fec/tests/m8/dotted/main.fe @@ -0,0 +1,4 @@ +unit main; +import acme.math; + +fn main() -> i32 { return math.answer(); } diff --git a/fec/tests/m8/pubpriv/lib.fe b/fec/tests/m8/pubpriv/lib.fe new file mode 100644 index 0000000..0e30a9b --- /dev/null +++ b/fec/tests/m8/pubpriv/lib.fe @@ -0,0 +1,5 @@ +unit lib; + +struct Hidden { value: i32, } +// ERROR:4:private +pub fn make() -> Hidden { return Hidden{ value: 1 }; } diff --git a/fec/tests/m8/pubpriv/main.fe b/fec/tests/m8/pubpriv/main.fe new file mode 100644 index 0000000..c53e7c5 --- /dev/null +++ b/fec/tests/m8/pubpriv/main.fe @@ -0,0 +1,4 @@ +unit main; +import lib; + +fn main() -> void { let value = lib.make(); } diff --git a/fec/tests/m9/README.md b/fec/tests/m9/README.md index ba80d35..5fec504 100644 --- a/fec/tests/m9/README.md +++ b/fec/tests/m9/README.md @@ -3,7 +3,7 @@ M9 adds comptime type parameters and monomorphization. `ok*.fe` must compile; `bad*.fe` must fail according to the first-line marker. -`defscope/` is a multi-unit definition-scope test and requires M8 imports. +`defscope/` and `okscope/` are multi-unit definition-scope tests and require M8 imports. `okscope/` verifies that an exported generic may use its definition unit's private helper through `.fei` support metadata. Coverage: - generic functions and structs, @@ -15,5 +15,7 @@ Coverage: - runtime `type` values forbidden, - definition-unit name lookup, - recursive instantiation depth limit. +- rejection of user value generics and generic type inference, +- same-instance recursive request reuse and selected-out `comptime if` semantic skipping. M9's DOS gate should additionally inspect generated `fe_generics.c`: `okdedup.fe` must emit one body for the repeated `(id, i32)` instantiation. diff --git a/fec/tests/m9/baddist.fe b/fec/tests/m9/baddist.fe new file mode 100644 index 0000000..bd1425e --- /dev/null +++ b/fec/tests/m9/baddist.fe @@ -0,0 +1,10 @@ +// ERROR:7:depth +unit baddist; + +struct Box(T) { value: T, } + +fn grow(comptime T: type) -> void { + grow(Box(T)); +} + +fn bad() -> void { grow(i32); } diff --git a/fec/tests/m9/badinfer.fe b/fec/tests/m9/badinfer.fe new file mode 100644 index 0000000..8bcccb8 --- /dev/null +++ b/fec/tests/m9/badinfer.fe @@ -0,0 +1,8 @@ +// ERROR:7:generic +unit badinfer; + +fn id(comptime T: type, value: T) -> T { return value; } + +fn bad() -> i32 { + return id(7); +} diff --git a/fec/tests/m9/badvalue.fe b/fec/tests/m9/badvalue.fe new file mode 100644 index 0000000..1e8ff97 --- /dev/null +++ b/fec/tests/m9/badvalue.fe @@ -0,0 +1,4 @@ +// ERROR:4:comptime +unit badvalue; + +fn id(comptime N: usize, value: i32) -> i32 { return value; } diff --git a/fec/tests/m9/oksamrec.fe b/fec/tests/m9/oksamrec.fe new file mode 100644 index 0000000..2419703 --- /dev/null +++ b/fec/tests/m9/oksamrec.fe @@ -0,0 +1,7 @@ +unit oksamrec; + +fn repeat(comptime T: type, value: T) -> T { + return repeat(T, value); +} + +fn test() -> i32 { return repeat(i32, 1); } diff --git a/fec/tests/m9/badscope/lib.fe b/fec/tests/m9/okscope/lib.fe similarity index 81% rename from fec/tests/m9/badscope/lib.fe rename to fec/tests/m9/okscope/lib.fe index bba2221..002e41b 100644 --- a/fec/tests/m9/badscope/lib.fe +++ b/fec/tests/m9/okscope/lib.fe @@ -1,4 +1,3 @@ -// ERROR:5:helper unit lib; pub fn call(comptime T: type, v: T) -> T { diff --git a/fec/tests/m9/badscope/main.fe b/fec/tests/m9/okscope/main.fe similarity index 100% rename from fec/tests/m9/badscope/main.fe rename to fec/tests/m9/okscope/main.fe diff --git a/fec/tests/m9/okskip.fe b/fec/tests/m9/okskip.fe new file mode 100644 index 0000000..e5fd24c --- /dev/null +++ b/fec/tests/m9/okskip.fe @@ -0,0 +1,11 @@ +unit okskip; + +fn select(comptime T: type, value: T) -> T { + comptime if T == i32 { + return value; + } else { + return unknown(value); + } +} + +fn test() -> i32 { return select(i32, 1); } From aba9370ea6e0fd6a90e17e1eb08fe7d4a18be6c5 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 20:46:45 +0900 Subject: [PATCH 055/184] implement M6 borrow checker integration --- fec/src/check.c | 488 +++++++++++++++++++++++++++++++++++++++++++++-- fec/src/own.c | 5 +- fec/src/parser.c | 2 +- 3 files changed, 481 insertions(+), 14 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index 88d6588..01a9396 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -15,6 +15,14 @@ struct FeSym { int initialized; int moved; FeNode *decl; + /* M6 ownership is tracked at the root local/parameter. A reference + binding remembers that root so releasing the binding's last use can + release the root borrow without a separate alias engine. */ + FeOwnState own; + FeSym *borrow_root; + int borrow_mut; + int borrow_defer; + FeScope *owner; }; struct FeScope { @@ -33,6 +41,8 @@ typedef struct FeCheckerState { FeType *ret; unsigned loop_depth; unsigned defer_depth; + FeOwnLiveness liveness; + FeNode *fn_node; } FeCheckerState; static FeType *unknown(FeCheck *c) @@ -55,6 +65,30 @@ static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) FeSym *sym=0; if (n && n->kind==FE_N_IDENT) sym=find_symbol(s->scope,n->text ? n->text : ""); + if (s->defer_depth != 0) { + /* A defer capture keeps the owner live until scope cleanup; its body + is not an immediate consuming use. */ + fe_own_mark_consumed(s->c->diags, + sym ? &sym->moved : 0, + sym ? sym->decl : 0, + n,t,1); + return; + } + if (sym && t && !fe_own_is_copy_type(t)) { + if (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX) { + fe_diag_error(s->c->diags,n->loc, + "cannot move a non-Copy value out of a projection; use mem.replace"); + return; + } + if (fe_own_access(s->c->diags,&sym->own,FE_OWN_MOVE,n->loc)) { + sym->moved=sym->own.move; + /* Keep the existing emitter contract: ownership-consuming AST + uses carry this flag, while FeOwnState is the diagnostic + authority. */ + fe_own_mark_consumed(s->c->diags,&sym->moved,sym->decl,n,t,0); + } + return; + } fe_own_mark_consumed(s->c->diags, sym ? &sym->moved : 0, sym ? sym->decl : 0, @@ -212,6 +246,11 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, sym->initialized = initialized; sym->moved = FE_OWN_AVAILABLE; sym->decl = decl; + fe_own_state_init(&sym->own, initialized); + sym->borrow_root = 0; + sym->borrow_mut = 0; + sym->borrow_defer = 0; + sym->owner = scope; if (decl) { decl->cname = cname; decl->sem_type = type; @@ -264,6 +303,8 @@ typedef struct FeFlowSlot { FeSym *sym; int moved; int initialized; + int own_move; + int own_initialized; } FeFlowSlot; static unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap) @@ -276,6 +317,8 @@ static unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap) slots[count].sym=&p->items[i]; slots[count].moved=p->items[i].moved; slots[count].initialized=p->items[i].initialized; + slots[count].own_move=p->items[i].own.move; + slots[count].own_initialized=p->items[i].own.initialized; ++count; } return count; @@ -287,6 +330,8 @@ static void flow_restore(FeFlowSlot *slots, unsigned count) for (i=0; imoved=slots[i].moved; slots[i].sym->initialized=slots[i].initialized; + slots[i].sym->own.move=slots[i].own_move; + slots[i].sym->own.initialized=slots[i].own_initialized; } } @@ -297,6 +342,237 @@ static void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, for (i=0; imoved=fe_own_merge_move(left[i].moved,right[i].moved); base[i].sym->initialized=left[i].initialized && right[i].initialized; + base[i].sym->own.move=fe_own_merge_move(left[i].own_move,right[i].own_move); + base[i].sym->own.initialized=left[i].own_initialized && right[i].own_initialized; + } +} + +static FeSym *own_root_symbol(FeCheckerState *s, FeNode *expr) +{ + FeOwnPlace place; + if (!fe_own_place_from_expr(expr,&place)) return 0; + return find_symbol(s->scope,place.root->text ? place.root->text : ""); +} + +static int own_is_global(FeCheckerState *s, FeSym *sym) +{ + FeScope *p; + if (!s || !sym) return 0; + for (p=s->globals; p; p=p->parent) { + unsigned i; + for (i=0;icount;++i) if (&p->items[i]==sym) return 1; + } + return 0; +} + +static void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) +{ + FeSym *root=own_root_symbol(s,expr); + if (!root) return; + if (mutable && root->type && root->type->kind==FE_TYPE_REF && + !root->type->ref_mut) { + err(s->c,expr->loc,"cannot create mutable borrow from a shared reference"); + return; + } + if (own_is_global(s,root) && + !(root->decl && root->decl->kind==FE_N_GLOBAL && + (root->decl->flags & 2U) && !mutable)) { + err(s->c,expr->loc,"cannot borrow a mutable global"); + return; + } + fe_own_access(s->c->diags,&root->own, + mutable ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + expr->loc); +} + +static void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) +{ + FeSym *root; + if (!expr || expr->kind!=FE_N_UNARY || !expr->text) return; + if (strcmp(expr->text,"&")!=0 && strcmp(expr->text,"&mut")!=0) return; + root=own_root_symbol(s,expr->a); + if (!root) return; + if (strcmp(expr->text,"&mut")==0) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); +} + +/* Return-reference provenance is represented at call sites by retaining a + borrow of the unique reference-derived argument (or method receiver). */ +static FeSym *own_derived_call_root(FeCheckerState *s, FeNode *call) +{ + FeNode *param; + FeNode *arg; + FeNode *source=0; + unsigned refs=0; + if (!call || call->kind!=FE_N_CALL || !call->sem_type || + !fe_own_is_reference_like(call->sem_type)) return 0; + if (call->a && call->a->kind==FE_N_MEMBER && call->sem_decl) { + param=call->sem_decl->a ? call->sem_decl->a->children : 0; + if (param && param->text && strcmp(param->text,"self")==0) + return own_root_symbol(s,call->a->a); + } + if (!call->sem_decl) return 0; + param=call->sem_decl->a ? call->sem_decl->a->children : 0; + arg=call->children; + while (param && arg) { + FeType *t=node_type(s->c,param->a); + if (fe_own_is_reference_like(t)) { ++refs; source=arg; } + param=param->next; + arg=arg->next; + } + return refs==1 ? own_root_symbol(s,source) : 0; +} + +static void own_bind_derived_call(FeCheckerState *s, FeSym *binding, + FeNode *value) +{ + FeSym *root; + if (!binding || !value || value->kind!=FE_N_CALL) return; + root=own_derived_call_root(s,value); + if (!root) return; /* Static provenance. */ + if (root->borrow_root) root=root->borrow_root; + if (value->sem_type->kind==FE_TYPE_REF && value->sem_type->ref_mut) + fe_own_access(s->c->diags,&root->own,FE_OWN_BORROW_MUT,value->loc); + else + fe_own_access(s->c->diags,&root->own,FE_OWN_BORROW_SHARED,value->loc); + binding->borrow_root=root; + binding->borrow_mut=value->sem_type->kind==FE_TYPE_REF && value->sem_type->ref_mut; +} + +static int own_stmt_uses(FeNode *node, const char *name) +{ + FeNode *x; + if (!node || !name) return 0; + if (node->kind==FE_N_IDENT && node->text && strcmp(node->text,name)==0) + return 1; + if (own_stmt_uses(node->a,name) || own_stmt_uses(node->b,name) || + own_stmt_uses(node->c,name)) return 1; + for (x=node->children;x;x=x->next) + if (own_stmt_uses(x,name)) return 1; + return 0; +} + +static int own_defer_uses(FeNode *node, const char *name) +{ + FeNode *x; + if (!node) return 0; + if (node->kind==FE_N_DEFER && own_stmt_uses(node->a,name)) return 1; + if (own_defer_uses(node->a,name) || own_defer_uses(node->b,name) || + own_defer_uses(node->c,name)) return 1; + for (x=node->children;x;x=x->next) + if (own_defer_uses(x,name)) return 1; + return 0; +} + +static int own_contains_node(FeNode *node, FeNode *needle) +{ + FeNode *x; + if (!node || !needle) return 0; + if (node==needle) return 1; + if (own_contains_node(node->a,needle) || + own_contains_node(node->b,needle) || + own_contains_node(node->c,needle)) return 1; + for (x=node->children;x;x=x->next) + if (own_contains_node(x,needle)) return 1; + return 0; +} + +static void own_release_after_stmt(FeCheckerState *s, FeScope *scope, + FeNode *stmt, int scope_end) +{ + unsigned i; + FeScope *p; + const FeOwnLastUse *last; + for (p=scope;p;p=scope_end ? 0 : p->parent) for (i=0;icount;++i) { + FeSym *ref=&p->items[i]; + if (!ref->borrow_root) continue; + last=fe_own_last_use(&s->liveness, + ref->decl && ref->decl->text ? ref->decl->text : ref->name); + if (!scope_end && (ref->borrow_defer || !last || last->defer_extended || + !own_contains_node(stmt,last->last_node))) continue; + if (ref->borrow_mut) fe_own_release_exclusive(&ref->borrow_root->own); + else fe_own_release_shared(&ref->borrow_root->own); + ref->borrow_root=0; + } +} + +/* Full borrow snapshots live in the AST arena, rather than on the 16-bit + compiler stack. The compact FeFlowSlot arrays retain the pre-M6 move and + initialization flow handling. */ +static FeOwnState *flow_own_new(FeCheckerState *s, unsigned count) +{ + if (!s || !count) return 0; + return (FeOwnState *)fe_arena_alloc(&s->c->ast->arena, + count*sizeof(FeOwnState)); +} + +static void flow_own_capture(FeFlowSlot *slots, FeOwnState *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iown; +} + +static void flow_own_restore(FeFlowSlot *slots, FeOwnState *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iown=states[i]; +} + +static void flow_own_merge(FeFlowSlot *slots, FeOwnState *left, + FeOwnState *right, unsigned count) +{ + unsigned i; + if (!left || !right) return; + for (i=0;iown=fe_own_merge_state(left[i],right[i]); +} + +typedef struct FeFlowBorrow { + FeSym *root; + int mutable; +} FeFlowBorrow; + +static FeFlowBorrow *flow_borrow_new(FeCheckerState *s, unsigned count) +{ + if (!s || !count) return 0; + return (FeFlowBorrow *)fe_arena_alloc(&s->c->ast->arena, + count*sizeof(FeFlowBorrow)); +} + +static void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iborrow_root; + states[i].mutable=slots[i].sym->borrow_mut; + } +} + +static void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count) +{ + unsigned i; + if (!states) return; + for (i=0;iborrow_root=states[i].root; + slots[i].sym->borrow_mut=states[i].mutable; + } +} + +static void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, + FeFlowBorrow *right, unsigned count) +{ + unsigned i; + if (!left || !right) return; + for (i=0;iborrow_root=left[i].root ? left[i].root : right[i].root; + slots[i].sym->borrow_mut=left[i].mutable || right[i].mutable; } } @@ -544,9 +820,10 @@ static FeType *check_identifier(FeCheckerState *s, FeNode *n, int read) } n->cname = sym->cname; n->sem_type = sym->type; - fe_own_check_use(s->c->diags,sym->moved,n->loc); - if (read && !sym->initialized && !sym->fn) - err(s->c, n->loc, "use of uninitialized variable"); + if (!sym->fn) { + fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); + sym->moved=sym->own.move; + } return sym->type; } @@ -601,6 +878,9 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) a=unknown(c); } } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + if (strcmp(op,"&mut")==0 && a && a->kind==FE_TYPE_REF && !a->ref_mut) + err(c,n->loc,"cannot create mutable borrow from a shared reference"); + own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); a=fe_type_ref(&c->types,a,strcmp(op,"&mut")==0); } n->sem_type = a; @@ -761,6 +1041,13 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) fe_type_intern(&c->types,"void"); return n->sem_type; } + if (et && (et->kind==FE_TYPE_SLICE || et->kind==FE_TYPE_STR) && + n->a->b && n->a->b->text && + strcmp(n->a->b->text,"trim")==0) { + if (n->children) err(c,n->loc,"trim takes no arguments"); + n->sem_type=fe_type_slice(&c->types,et->elem); + return n->sem_type; + } variant=et && et->kind==FE_TYPE_ENUM ? fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; arg=n->children; @@ -787,13 +1074,24 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) arg = n->children; while (param && arg) { a = check_expr(s, arg); - mark_moved(s,arg,a); b = node_type(c, param->a); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (b && a && b->kind==FE_TYPE_SLICE && !b->ref_mut && + a->kind==FE_TYPE_SLICE && a->ref_mut) { + /* Call-only []mut -> [] weakening is a temporary view. */ + } else mark_moved(s,arg,a); if (!compatible(b, a, arg) && !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && a->kind != FE_TYPE_UNKNOWN) err(c, arg->loc, "argument type mismatch"); + own_release_temporary_borrow(s,arg); param = param->next; arg = arg->next; } @@ -869,8 +1167,10 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) err(s->c, n->loc, "cannot assign to immutable let"); n->cname = sym->cname; n->sem_type = sym->type; - if (read && !sym->initialized) - err(s->c, n->loc, "use of uninitialized variable"); + if (read) { + fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); + sym->moved=sym->own.move; + } return sym->type; } if (n && n->kind == FE_N_MEMBER) { @@ -1090,6 +1390,64 @@ static void check_type_cycles(FeCheck *c) for (t=c->types.types;t;t=t->next) check_type_cycle(c,t); } +static int own_ast_reference_type(FeNode *type) +{ + if (!type || !type->text) return 0; + return strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || + (strcmp(type->text,"[")==0 && !type->a) || strcmp(type->text,"str")==0; +} + +static int own_ast_pointer_to_reference(FeNode *type) +{ + return type && type->text && strcmp(type->text,"*")==0 && + own_ast_reference_type(type->a); +} + +static void check_reference_storage(FeCheck *c, FeNode *decl) +{ + FeNode *m; + if (!decl) return; + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + for (m=decl->children;m;m=m->next) + if (m->kind==FE_N_FIELD && + (own_ast_reference_type(m->a) || own_ast_pointer_to_reference(m->a))) + err(c,m->loc,"reference type is not allowed in aggregate storage"); + } + if ((decl->kind==FE_N_GLOBAL || decl->kind==FE_N_CONST) && decl->a && + own_ast_reference_type(decl->a) && + !(decl->kind==FE_N_CONST && decl->a->text && strcmp(decl->a->text,"str")==0)) + err(c,decl->loc,"reference type is not allowed in global storage"); + if (decl->kind==FE_N_FN && decl->b && own_ast_pointer_to_reference(decl->b)) + err(c,decl->b->loc,"reference type is not allowed as a pointer target"); + if (decl->kind==FE_N_FN) + for (m=decl->a ? decl->a->children : 0;m;m=m->next) + if (own_ast_pointer_to_reference(m->a)) + err(c,m->loc,"reference type is not allowed as a pointer target"); +} + +static int own_return_from_allowed_root(FeCheckerState *s, FeNode *expr) +{ + FeSym *root; + FeNode *p; + unsigned refs=0; + if (!expr) return 0; + root=own_root_symbol(s,expr); + if (!root) return 1; /* Static-producing builtins/methods are checked by + their declared R8 interface. */ + if (own_is_global(s,root)) + return root->decl && root->decl->kind==FE_N_GLOBAL && + (root->decl->flags & 2U); + if (!root->decl || root->decl->kind!=FE_N_PARAM) return 0; + for (p=s->fn_node && s->fn_node->a ? s->fn_node->a->children : 0; + p;p=p->next) { + FeType *t=p->sem_type ? p->sem_type : node_type(s->c,p->a); + if (fe_own_is_reference_like(t)) ++refs; + } + if (s->fn_node && s->fn_node->text && refs && + root->name && strcmp(root->name,"self")==0) return 1; + return refs==1; +} + static void check_stmt(FeCheckerState *s, FeNode *n) { FeCheck *c = s->c; @@ -1104,7 +1462,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n) case FE_N_BLOCK: old = s->scope; s->scope = scope_new(s, old); - for (x = n->children; x; x = x->next) check_stmt(s, x); + for (x = n->children; x; x = x->next) { + check_stmt(s,x); + own_release_after_stmt(s,s->scope,x,0); + } + own_release_after_stmt(s,s->scope,n,1); s->scope = old; break; case FE_N_LET: @@ -1121,8 +1483,16 @@ static void check_stmt(FeCheckerState *s, FeNode *n) if (n->kind==FE_N_LET && a->kind==FE_TYPE_SLICE && a->ref_mut) err(c,n->loc,"let cannot bind a mutable slice"); mark_moved(s,n->b,b); - add_symbol(s, s->scope, n->text, a, 0, 0, 1, + sym=add_symbol(s, s->scope, n->text, a, 0, 0, 1, local_cname(c, n->text ? n->text : "local"), n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth != 0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); break; case FE_N_VAR: a = n->a ? node_type(c, n->a) : unknown(c); @@ -1138,8 +1508,16 @@ static void check_stmt(FeCheckerState *s, FeNode *n) err(c, n->loc, "void expression cannot initialize a variable"); mark_moved(s,n->b,b); initialized = n->b != 0; - add_symbol(s, s->scope, n->text, a, 0, 1, initialized, + sym=add_symbol(s, s->scope, n->text, a, 0, 1, initialized, local_cname(c, n->text ? n->text : "local"), n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth != 0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); break; case FE_N_ASSIGN: b = check_expr(s, n->b); @@ -1149,7 +1527,26 @@ static void check_stmt(FeCheckerState *s, FeNode *n) mark_moved(s,n->b,b); sym = n->a && n->a->kind == FE_N_IDENT ? find_symbol(s->scope, n->a->text) : 0; - if (sym && sym->mutable) sym->initialized = 1; + if (sym && sym->mutable) { + sym->initialized = 1; + fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); + sym->moved=sym->own.move; + if (n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && + fe_own_is_reference_like(sym->type)) { + FeSym *root=own_root_symbol(s,n->b->a); + if (root && root->owner!=sym->owner) + err(c,n->b->loc,"reference would outlive its source scope"); + else if (root) { + if (sym->borrow_root) { + if (sym->borrow_mut) fe_own_release_exclusive(&sym->borrow_root->own); + else fe_own_release_shared(&sym->borrow_root->own); + } + sym->borrow_root=root; + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + } + } + } break; case FE_N_EXPR_STMT: check_expr(s, n->a); @@ -1165,50 +1562,104 @@ static void check_stmt(FeCheckerState *s, FeNode *n) break; case FE_N_IF: { FeFlowSlot base[64], left[64], right[64]; + FeOwnState *own_base, *own_left, *own_right; + FeFlowBorrow *borrow_base, *borrow_left, *borrow_right; unsigned flow_count; a = check_expr(s, n->a); if (known(a) && a->kind != FE_TYPE_BOOL) err(c, n->loc, "if condition must be bool"); flow_count=flow_capture(s->scope,base,64); + own_base=flow_own_new(s,flow_count); + own_left=flow_own_new(s,flow_count); + own_right=flow_own_new(s,flow_count); + borrow_base=flow_borrow_new(s,flow_count); + borrow_left=flow_borrow_new(s,flow_count); + borrow_right=flow_borrow_new(s,flow_count); + flow_own_capture(base,own_base,flow_count); + flow_borrow_capture(base,borrow_base,flow_count); check_stmt(s, n->b); flow_capture(s->scope,left,flow_count); + flow_own_capture(left,own_left,flow_count); + flow_borrow_capture(left,borrow_left,flow_count); flow_restore(base,flow_count); + flow_own_restore(base,own_base,flow_count); + flow_borrow_restore(base,borrow_base,flow_count); if (n->c) check_stmt(s, n->c); - if (n->c) flow_capture(s->scope,right,flow_count); + if (n->c) { + flow_capture(s->scope,right,flow_count); + flow_own_capture(right,own_right,flow_count); + flow_borrow_capture(right,borrow_right,flow_count); + } else { unsigned i; - for (i=0;ia); if (known(a) && a->kind != FE_TYPE_BOOL) err(c, n->loc, "while condition must be bool"); flow_count=flow_capture(s->scope,base,64); + own_base=flow_own_new(s,flow_count); + own_body=flow_own_new(s,flow_count); + own_entry2=flow_own_new(s,flow_count); + borrow_base=flow_borrow_new(s,flow_count); + borrow_body=flow_borrow_new(s,flow_count); + borrow_entry2=flow_borrow_new(s,flow_count); + flow_own_capture(base,own_base,flow_count); + flow_borrow_capture(base,borrow_base,flow_count); if (s->loop_depth < 255U) ++s->loop_depth; check_stmt(s, n->b); if (s->loop_depth) --s->loop_depth; flow_capture(s->scope,body,flow_count); + flow_own_capture(body,own_body,flow_count); + flow_borrow_capture(body,borrow_body,flow_count); for (i=0;iloop_depth < 255U) ++s->loop_depth; check_stmt(s,n->b); if (s->loop_depth) --s->loop_depth; flow_capture(s->scope,body,flow_count); + flow_own_capture(body,own_body,flow_count); + flow_borrow_capture(body,borrow_body,flow_count); for(i=0;ia ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); + if (s->ret && fe_own_is_reference_like(s->ret) && + !own_return_from_allowed_root(s,n->a)) + err(c,n->loc,"reference return must be derived from a parameter or static"); mark_moved(s,n->a,b); if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) err(c, n->loc, "void expression returned from value function"); @@ -1253,6 +1707,9 @@ static void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) s.ret = fn->b ? node_type(c, fn->b) : fe_type_intern(&c->types, "void"); s.loop_depth=0; s.defer_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->ast->arena); + fe_own_collect_last_uses(&s.liveness,fn); fn->sem_type = s.ret; for (x = fn->a ? fn->a->children : 0; x; x = x->next) { t = node_type(c, x->a); @@ -1278,6 +1735,9 @@ static void check_method(FeCheck *c, FeNode *fn, FeScope *globals, s.ret=fn->b ? method_type(c,fn->b,owner) : fe_type_intern(&c->types,"void"); s.loop_depth=0; s.defer_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->ast->arena); + fe_own_collect_last_uses(&s.liveness,fn); fn->sem_type=s.ret; for(x=fn->a ? fn->a->children : 0; x; x=x->next) { t=method_type(c,x->a,owner); @@ -1302,6 +1762,8 @@ int fe_check_program(FeCheck *c) for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) if (n->kind == FE_N_STRUCT) fe_type_declare_struct(&c->types, n, (n->flags & 1U) != 0); + for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) + check_reference_storage(c,n); for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) if (n->kind == FE_N_ENUM) fe_type_declare_enum(&c->types, n); for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) @@ -1370,5 +1832,7 @@ FeType *fe_check_expr_type(FeCheck *c, FeNode *n) s.ret = fe_type_intern(&c->types, "void"); s.loop_depth=0; s.defer_depth=0; + s.fn_node=0; + fe_own_liveness_init(&s.liveness,&c->ast->arena); return check_expr(&s, n); } diff --git a/fec/src/own.c b/fec/src/own.c index f40fd43..ee37c9b 100644 --- a/fec/src/own.c +++ b/fec/src/own.c @@ -314,7 +314,10 @@ static int fe_own_live_add(FeOwnLiveness *live, FeNode *decl) FeOwnLastUse *slot; unsigned capacity; const char *cname; - if (!live || !decl || !live->arena || !fe_own_is_reference_like(decl->sem_type)) + /* This prepass runs before local inference. Recording every local is + harmless (only reference bindings consume the result) and lets an + inferred `let r = &x` participate in last-use analysis. */ + if (!live || !decl || !live->arena) return 1; cname = decl->cname ? decl->cname : decl->text; if (!cname || fe_own_live_find(live, cname)) return 1; diff --git a/fec/src/parser.c b/fec/src/parser.c index 2e9366d..8d8f8f4 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -221,7 +221,7 @@ static FeNode *decl(FeParser *p) if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } - if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } + if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(kk==FE_TOK_STATIC)n->flags|=2U;if(shared)n->flags|=4U;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } error(p,"expected declaration"); before=p->current.kind; recover(p); if (p->current.kind==before && p->current.kind!=FE_TOK_EOF) next(p); return 0; From 9c3cae740664a5043bacfa0b82eace8ed8239bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:16:14 +0900 Subject: [PATCH 056/184] add M7 semantic foundation --- fec/src/m7.h | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 fec/src/m7.h diff --git a/fec/src/m7.h b/fec/src/m7.h new file mode 100644 index 0000000..ad0f968 --- /dev/null +++ b/fec/src/m7.h @@ -0,0 +1,31 @@ +#ifndef FE_M7_H +#define FE_M7_H + +#include "types.h" + +typedef enum FeM7ContextKind { + FE_M7_CONTEXT_NONE = 0, + FE_M7_CONTEXT_SUCCESS, + FE_M7_CONTEXT_FAILURE +} FeM7ContextKind; + +typedef enum FeM7LazyKind { + FE_M7_LAZY_NONE = 0, + FE_M7_LAZY_ORELSE, + FE_M7_LAZY_CATCH +} FeM7LazyKind; + +FeType *fe_m7_optional_type(FeTypeCtx *ctx, FeType *payload); +int fe_m7_optional_uses_niche(const FeType *payload); +int fe_m7_can_contextual_null(const FeType *expected); + +FeType *fe_m7_error_union_type(FeTypeCtx *ctx, FeType *error_type, + FeType *value_type); +FeType *fe_m7_error_type(FeTypeCtx *ctx, const FeType *error_union); +FeM7ContextKind fe_m7_error_context(FeTypeCtx *ctx, const FeType *expected, + const FeType *actual); + +FeM7LazyKind fe_m7_lazy_kind(const FeNode *node); +int fe_m7_is_try(const FeNode *node); + +#endif From c4f3accbb37016b5d880903e76c351736ab6743d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:16:30 +0900 Subject: [PATCH 057/184] implement M7 semantic foundation --- fec/src/m7.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 fec/src/m7.c diff --git a/fec/src/m7.c b/fec/src/m7.c new file mode 100644 index 0000000..a314fe3 --- /dev/null +++ b/fec/src/m7.c @@ -0,0 +1,114 @@ +#include "m7.h" +#include +#include + +static char *m7_generated_name(FeTypeCtx *ctx, const char *prefix) +{ + char number[24]; + char *p; + unsigned long n; + sprintf(number, "%u", ctx->generated_serial++); + n = (unsigned long)strlen(prefix) + (unsigned long)strlen(number) + 1UL; + p = (char *)fe_arena_alloc(ctx->arena, n); + if (!p) return 0; + strcpy(p, prefix); + strcat(p, number); + return p; +} + +int fe_m7_optional_uses_niche(const FeType *payload) +{ + if (!payload) return 0; + if (payload->kind == FE_TYPE_REF) return 1; + if (payload->kind == FE_TYPE_OWNED && + !(payload->elem && payload->elem->kind == FE_TYPE_SLICE)) + return 1; + return 0; +} + +FeType *fe_m7_optional_type(FeTypeCtx *ctx, FeType *payload) +{ + char key[128]; + FeType *t; + if (!ctx || !payload) return 0; + sprintf(key, "?%s", payload->name); + t = fe_type_intern(ctx, key); + if (!t) return 0; + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_OPTIONAL; + t->elem = payload; + if (!fe_m7_optional_uses_niche(payload)) { + t->cname = m7_generated_name(ctx, "struct fe_option_"); + t->maker = m7_generated_name(ctx, "fe_make_option_"); + } + } + return t; +} + +int fe_m7_can_contextual_null(const FeType *expected) +{ + return expected && expected->kind == FE_TYPE_OPTIONAL; +} + +FeType *fe_m7_error_union_type(FeTypeCtx *ctx, FeType *error_type, + FeType *value_type) +{ + char key[160]; + FeType *t; + if (!ctx || !value_type) return 0; + if (!error_type || strcmp(error_type->name, "core.Error") == 0) + return fe_type_error_union(ctx, value_type); + sprintf(key, "%s!%s", error_type->name, value_type->name); + t = fe_type_intern(ctx, key); + if (!t) return 0; + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_ERROR_UNION; + /* For FE_TYPE_ERROR_UNION, elem is the nominal error identity. + A null elem denotes the built-in core.Error shorthand !T. */ + t->elem = error_type; + t->error_value = value_type; + if (value_type->kind != FE_TYPE_VOID) { + t->cname = m7_generated_name(ctx, "struct fe_result_"); + t->maker = m7_generated_name(ctx, "fe_make_result_"); + t->alloc_cname = m7_generated_name(ctx, "fe_alloc_result_"); + } + } + return t; +} + +FeType *fe_m7_error_type(FeTypeCtx *ctx, const FeType *error_union) +{ + if (!ctx || !error_union || error_union->kind != FE_TYPE_ERROR_UNION) + return 0; + if (error_union->elem) return error_union->elem; + return fe_type_intern(ctx, "core.Error"); +} + +FeM7ContextKind fe_m7_error_context(FeTypeCtx *ctx, const FeType *expected, + const FeType *actual) +{ + FeType *error_type; + if (!ctx || !expected || !actual || + expected->kind != FE_TYPE_ERROR_UNION) + return FE_M7_CONTEXT_NONE; + if (expected->error_value && fe_type_equal(expected->error_value, actual)) + return FE_M7_CONTEXT_SUCCESS; + error_type = fe_m7_error_type(ctx, expected); + if (error_type && fe_type_equal(error_type, actual)) + return FE_M7_CONTEXT_FAILURE; + return FE_M7_CONTEXT_NONE; +} + +FeM7LazyKind fe_m7_lazy_kind(const FeNode *node) +{ + if (!node || !node->text) return FE_M7_LAZY_NONE; + if (strcmp(node->text, "orelse") == 0) return FE_M7_LAZY_ORELSE; + if (strcmp(node->text, "catch") == 0) return FE_M7_LAZY_CATCH; + return FE_M7_LAZY_NONE; +} + +int fe_m7_is_try(const FeNode *node) +{ + return node && node->kind == FE_N_UNARY && node->text && + strcmp(node->text, "try") == 0; +} From 40ec131ffc7f4ccea84c57162efe56221a3b0140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:16:46 +0900 Subject: [PATCH 058/184] add cleanup lowering plan --- fec/src/lower.h | 61 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 fec/src/lower.h diff --git a/fec/src/lower.h b/fec/src/lower.h new file mode 100644 index 0000000..bb2e544 --- /dev/null +++ b/fec/src/lower.h @@ -0,0 +1,61 @@ +#ifndef FE_LOWER_H +#define FE_LOWER_H + +#include "types.h" + +#define FE_LOWER_NO_SCOPE ((unsigned)~0U) + +typedef enum FeLowerExitKind { + FE_LOWER_EXIT_FALLTHROUGH = 0, + FE_LOWER_EXIT_RETURN, + FE_LOWER_EXIT_ERROR_RETURN, + FE_LOWER_EXIT_BREAK, + FE_LOWER_EXIT_CONTINUE +} FeLowerExitKind; + +typedef enum FeLowerCleanupKind { + FE_LOWER_CLEANUP_DROP = 0, + FE_LOWER_CLEANUP_DEFER +} FeLowerCleanupKind; + +typedef struct FeLowerScope { + FeNode *block; + unsigned parent; + unsigned ordinal; +} FeLowerScope; + +typedef struct FeLowerCleanup { + FeLowerCleanupKind kind; + unsigned scope; + unsigned ordinal; + FeNode *node; + FeNode *decl; + FeType *type; +} FeLowerCleanup; + +typedef struct FeLowerPlan { + FeArena *arena; + FeNode *fn; + FeLowerScope *scopes; + unsigned scope_count; + unsigned scope_capacity; + FeLowerCleanup *cleanups; + unsigned cleanup_count; + unsigned cleanup_capacity; + unsigned next_ordinal; +} FeLowerPlan; + +void fe_lower_plan_init(FeLowerPlan *plan, FeArena *arena); +int fe_lower_plan_build(FeLowerPlan *plan, FeNode *fn); +unsigned fe_lower_scope_for_block(const FeLowerPlan *plan, + const FeNode *block); +unsigned fe_lower_collect_cleanups(const FeLowerPlan *plan, + const FeNode *from_block, + const FeNode *stop_block, + const FeLowerCleanup **out, + unsigned out_capacity); +int fe_lower_type_needs_drop(const FeType *type); +int fe_lower_exit_runs_cleanup(FeLowerExitKind kind); +int fe_lower_exit_leaves_function(FeLowerExitKind kind); + +#endif From 8aae7d9a6317d8a9e8adda96b35c6c20e6280d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:17:15 +0900 Subject: [PATCH 059/184] implement cleanup lowering plan --- fec/src/lower.c | 227 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 227 insertions(+) create mode 100644 fec/src/lower.c diff --git a/fec/src/lower.c b/fec/src/lower.c new file mode 100644 index 0000000..abd6942 --- /dev/null +++ b/fec/src/lower.c @@ -0,0 +1,227 @@ +#include "lower.h" +#include + +static int lower_grow_scopes(FeLowerPlan *plan) +{ + FeLowerScope *items; + unsigned capacity; + if (plan->scope_count < plan->scope_capacity) return 1; + capacity = plan->scope_capacity ? plan->scope_capacity * 2U : 8U; + items = (FeLowerScope *)fe_arena_alloc(plan->arena, + capacity * sizeof(FeLowerScope)); + if (!items) return 0; + if (plan->scopes) + memcpy(items, plan->scopes, + plan->scope_count * sizeof(FeLowerScope)); + plan->scopes = items; + plan->scope_capacity = capacity; + return 1; +} + +static int lower_grow_cleanups(FeLowerPlan *plan) +{ + FeLowerCleanup *items; + unsigned capacity; + if (plan->cleanup_count < plan->cleanup_capacity) return 1; + capacity = plan->cleanup_capacity ? plan->cleanup_capacity * 2U : 16U; + items = (FeLowerCleanup *)fe_arena_alloc(plan->arena, + capacity * sizeof(FeLowerCleanup)); + if (!items) return 0; + if (plan->cleanups) + memcpy(items, plan->cleanups, + plan->cleanup_count * sizeof(FeLowerCleanup)); + plan->cleanups = items; + plan->cleanup_capacity = capacity; + return 1; +} + +static unsigned lower_add_scope(FeLowerPlan *plan, FeNode *block, + unsigned parent) +{ + FeLowerScope *scope; + unsigned index; + if (!lower_grow_scopes(plan)) return FE_LOWER_NO_SCOPE; + index = plan->scope_count++; + scope = &plan->scopes[index]; + scope->block = block; + scope->parent = parent; + scope->ordinal = plan->next_ordinal++; + return index; +} + +static int lower_add_cleanup(FeLowerPlan *plan, unsigned scope, + FeLowerCleanupKind kind, FeNode *node, + FeNode *decl, FeType *type) +{ + FeLowerCleanup *cleanup; + if (!lower_grow_cleanups(plan)) return 0; + cleanup = &plan->cleanups[plan->cleanup_count++]; + cleanup->kind = kind; + cleanup->scope = scope; + cleanup->ordinal = plan->next_ordinal++; + cleanup->node = node; + cleanup->decl = decl; + cleanup->type = type; + return 1; +} + +int fe_lower_type_needs_drop(const FeType *type) +{ + unsigned i; + unsigned j; + if (!type) return 0; + if (type->kind == FE_TYPE_OWNED) return 1; + if (type->kind == FE_TYPE_OPTIONAL) + return fe_lower_type_needs_drop(type->elem); + if (type->kind == FE_TYPE_ERROR_UNION) + return fe_lower_type_needs_drop(type->error_value); + if (type->kind == FE_TYPE_ARRAY) + return fe_lower_type_needs_drop(type->elem); + if (type->kind == FE_TYPE_STRUCT) { + if (type->has_drop) return 1; + for (i = 0; i < type->field_count; ++i) + if (fe_lower_type_needs_drop(type->fields[i].type)) return 1; + return 0; + } + if (type->kind == FE_TYPE_ENUM) { + for (i = 0; i < type->variant_count; ++i) + for (j = 0; j < type->variants[i].field_count; ++j) + if (fe_lower_type_needs_drop(type->variants[i].fields[j].type)) + return 1; + } + return 0; +} + +static int lower_build_node(FeLowerPlan *plan, FeNode *node, + unsigned scope); + +static int lower_build_list(FeLowerPlan *plan, FeNode *node, + unsigned scope) +{ + while (node) { + if (!lower_build_node(plan, node, scope)) return 0; + node = node->next; + } + return 1; +} + +static int lower_build_block(FeLowerPlan *plan, FeNode *block, + unsigned parent) +{ + unsigned scope; + if (!block || block->kind != FE_N_BLOCK) return 1; + scope = lower_add_scope(plan, block, parent); + if (scope == FE_LOWER_NO_SCOPE) return 0; + return lower_build_list(plan, block->children, scope); +} + +static int lower_build_node(FeLowerPlan *plan, FeNode *node, + unsigned scope) +{ + FeNode *child; + FeType *type; + if (!node) return 1; + if (node->kind == FE_N_BLOCK) + return lower_build_block(plan, node, scope); + if (node->kind == FE_N_DEFER) { + if (!lower_add_cleanup(plan, scope, FE_LOWER_CLEANUP_DEFER, + node->a, node, 0)) + return 0; + return lower_build_node(plan, node->a, scope); + } + if (node->kind == FE_N_LET || node->kind == FE_N_VAR || + node->kind == FE_N_CONST) { + type = node->sem_type; + if (fe_lower_type_needs_drop(type)) + if (!lower_add_cleanup(plan, scope, FE_LOWER_CLEANUP_DROP, + node, node, type)) + return 0; + } + if (!lower_build_node(plan, node->a, scope)) return 0; + if (!lower_build_node(plan, node->b, scope)) return 0; + if (!lower_build_node(plan, node->c, scope)) return 0; + for (child = node->children; child; child = child->next) + if (!lower_build_node(plan, child, scope)) return 0; + return 1; +} + +void fe_lower_plan_init(FeLowerPlan *plan, FeArena *arena) +{ + if (!plan) return; + plan->arena = arena; + plan->fn = 0; + plan->scopes = 0; + plan->scope_count = 0; + plan->scope_capacity = 0; + plan->cleanups = 0; + plan->cleanup_count = 0; + plan->cleanup_capacity = 0; + plan->next_ordinal = 0; +} + +int fe_lower_plan_build(FeLowerPlan *plan, FeNode *fn) +{ + if (!plan || !plan->arena || !fn || fn->kind != FE_N_FN) return 0; + plan->fn = fn; + plan->scopes = 0; + plan->scope_count = 0; + plan->scope_capacity = 0; + plan->cleanups = 0; + plan->cleanup_count = 0; + plan->cleanup_capacity = 0; + plan->next_ordinal = 0; + if (!fn->c) return 1; + return lower_build_block(plan, fn->c, FE_LOWER_NO_SCOPE); +} + +unsigned fe_lower_scope_for_block(const FeLowerPlan *plan, + const FeNode *block) +{ + unsigned i; + if (!plan || !block) return FE_LOWER_NO_SCOPE; + for (i = 0; i < plan->scope_count; ++i) + if (plan->scopes[i].block == block) return i; + return FE_LOWER_NO_SCOPE; +} + +unsigned fe_lower_collect_cleanups(const FeLowerPlan *plan, + const FeNode *from_block, + const FeNode *stop_block, + const FeLowerCleanup **out, + unsigned out_capacity) +{ + unsigned scope; + unsigned stop; + unsigned i; + unsigned count; + if (!plan || !from_block) return 0; + scope = fe_lower_scope_for_block(plan, from_block); + stop = stop_block ? fe_lower_scope_for_block(plan, stop_block) : + FE_LOWER_NO_SCOPE; + count = 0; + while (scope != FE_LOWER_NO_SCOPE && scope != stop) { + for (i = plan->cleanup_count; i > 0; --i) { + if (plan->cleanups[i - 1U].scope != scope) continue; + if (out && count < out_capacity) + out[count] = &plan->cleanups[i - 1U]; + ++count; + } + scope = plan->scopes[scope].parent; + } + return count; +} + +int fe_lower_exit_runs_cleanup(FeLowerExitKind kind) +{ + return kind == FE_LOWER_EXIT_FALLTHROUGH || + kind == FE_LOWER_EXIT_RETURN || + kind == FE_LOWER_EXIT_ERROR_RETURN || + kind == FE_LOWER_EXIT_BREAK || + kind == FE_LOWER_EXIT_CONTINUE; +} + +int fe_lower_exit_leaves_function(FeLowerExitKind kind) +{ + return kind == FE_LOWER_EXIT_RETURN || + kind == FE_LOWER_EXIT_ERROR_RETURN; +} From 47ef8420a93bca114cabfe22f0c13ceebf48d964 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:17:33 +0900 Subject: [PATCH 060/184] add optional type kind --- fec/src/types.h | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fec/src/types.h b/fec/src/types.h index 1974a46..5e1c984 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -6,7 +6,7 @@ typedef enum FeTypeKind { FE_TYPE_ERROR, FE_TYPE_ERROR_UNION, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, FE_TYPE_STRUCT, FE_TYPE_ENUM, FE_TYPE_ARRAY, FE_TYPE_SLICE, FE_TYPE_STR, - FE_TYPE_REF, FE_TYPE_OWNED, FE_TYPE_UNKNOWN + FE_TYPE_REF, FE_TYPE_OWNED, FE_TYPE_OPTIONAL, FE_TYPE_UNKNOWN } FeTypeKind; typedef struct FeFieldType FeFieldType; @@ -49,7 +49,9 @@ struct FeType { unsigned long size; unsigned align; FeType *elem; - /* Success value for an error union; !void is represented directly. */ + /* Success value for an error union; !void is represented directly. + For a nominal E!T created by M7, elem holds E. A null elem denotes + the built-in core.Error shorthand !T. */ FeType *error_value; int ref_mut; FeFieldType *fields; From bc9792532d150fe0a2472e22e86dca2ae88ec6bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:17:45 +0900 Subject: [PATCH 061/184] wire M7 foundation into host build --- fec/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/Makefile b/fec/Makefile index ae23bb3..a6eb4c5 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -std=c89 CPPFLAGS ?= -Isrc -SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/own.c src/check.c src/emit_c.c src/driver.c +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check.c src/lower.c src/emit_c.c src/driver.c OBJ = $(SRC:.c=.o) .PHONY: all clean dos-build From d43f28d06ae8bc9b1c254c4204083dc2e0ef6133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:17:57 +0900 Subject: [PATCH 062/184] wire M7 foundation into DOS build --- fec/build-dos.bat | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/fec/build-dos.bat b/fec/build-dos.bat index fbb7e5c..02425ec 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -26,10 +26,14 @@ wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=parser.obj src\parser.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=types.obj src\types.c if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=m7.obj src\m7.c +if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=own.obj src\own.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check.c if errorlevel 1 goto build_fail +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lower.obj src\lower.c +if errorlevel 1 goto build_fail rem Use an unambiguous short object name for the emit_c source. wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emit_c.c if errorlevel 1 goto build_fail From 8b1efe3562de8446b59692bde50d2d56308c890e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:32:34 +0900 Subject: [PATCH 063/184] complete M7 type metadata --- fec/src/types.h | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/fec/src/types.h b/fec/src/types.h index 5e1c984..c5ddd88 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -4,9 +4,10 @@ #include "ast.h" typedef enum FeTypeKind { - FE_TYPE_ERROR, FE_TYPE_ERROR_UNION, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, + FE_TYPE_ERROR, FE_TYPE_ERROR_UNION, FE_TYPE_OPTIONAL, + FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, FE_TYPE_STRUCT, FE_TYPE_ENUM, FE_TYPE_ARRAY, FE_TYPE_SLICE, FE_TYPE_STR, - FE_TYPE_REF, FE_TYPE_OWNED, FE_TYPE_OPTIONAL, FE_TYPE_UNKNOWN + FE_TYPE_REF, FE_TYPE_OWNED, FE_TYPE_UNKNOWN } FeTypeKind; typedef struct FeFieldType FeFieldType; @@ -33,6 +34,8 @@ struct FeType { char name[64]; char *cname; char *maker; + char *none_cname; + char *unwrap_cname; char *indexer; char *slicer; char *full_slicer; @@ -48,10 +51,10 @@ struct FeType { unsigned long length; unsigned long size; unsigned align; + /* Element/payload type for refs, owners, slices and optionals. For an + error union this is the nominal error identity; NULL means core.Error. */ FeType *elem; - /* Success value for an error union; !void is represented directly. - For a nominal E!T created by M7, elem holds E. A null elem denotes - the built-in core.Error shorthand !T. */ + /* Success value for an error union. */ FeType *error_value; int ref_mut; FeFieldType *fields; From 411c86afc223efcdc7e5a1587e6ea62d6bc1ecdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:32:46 +0900 Subject: [PATCH 064/184] extend M7 semantic helpers --- fec/src/m7.h | 1 + 1 file changed, 1 insertion(+) diff --git a/fec/src/m7.h b/fec/src/m7.h index ad0f968..fe0b078 100644 --- a/fec/src/m7.h +++ b/fec/src/m7.h @@ -27,5 +27,6 @@ FeM7ContextKind fe_m7_error_context(FeTypeCtx *ctx, const FeType *expected, FeM7LazyKind fe_m7_lazy_kind(const FeNode *node); int fe_m7_is_try(const FeNode *node); +int fe_m7_is_null(const FeNode *node); #endif From 8ac76aed3981ac79704e175dae7d57aec869baa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:33:02 +0900 Subject: [PATCH 065/184] extend M7 type construction --- fec/src/m7.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/fec/src/m7.c b/fec/src/m7.c index a314fe3..0cfd374 100644 --- a/fec/src/m7.c +++ b/fec/src/m7.c @@ -37,9 +37,12 @@ FeType *fe_m7_optional_type(FeTypeCtx *ctx, FeType *payload) if (t->kind == FE_TYPE_UNKNOWN) { t->kind = FE_TYPE_OPTIONAL; t->elem = payload; + t->unwrap_cname = m7_generated_name(ctx, "fe_unwrap_option_"); + t->drop_cname = m7_generated_name(ctx, "fe_drop_option_"); if (!fe_m7_optional_uses_niche(payload)) { t->cname = m7_generated_name(ctx, "struct fe_option_"); t->maker = m7_generated_name(ctx, "fe_make_option_"); + t->none_cname = m7_generated_name(ctx, "fe_none_option_"); } } return t; @@ -63,13 +66,13 @@ FeType *fe_m7_error_union_type(FeTypeCtx *ctx, FeType *error_type, if (!t) return 0; if (t->kind == FE_TYPE_UNKNOWN) { t->kind = FE_TYPE_ERROR_UNION; - /* For FE_TYPE_ERROR_UNION, elem is the nominal error identity. - A null elem denotes the built-in core.Error shorthand !T. */ t->elem = error_type; t->error_value = value_type; + t->drop_cname = m7_generated_name(ctx, "fe_drop_result_"); if (value_type->kind != FE_TYPE_VOID) { t->cname = m7_generated_name(ctx, "struct fe_result_"); t->maker = m7_generated_name(ctx, "fe_make_result_"); + t->none_cname = m7_generated_name(ctx, "fe_fail_result_"); t->alloc_cname = m7_generated_name(ctx, "fe_alloc_result_"); } } @@ -112,3 +115,9 @@ int fe_m7_is_try(const FeNode *node) return node && node->kind == FE_N_UNARY && node->text && strcmp(node->text, "try") == 0; } + +int fe_m7_is_null(const FeNode *node) +{ + return node && node->kind == FE_N_LITERAL && node->text && + strcmp(node->text, "null") == 0; +} From 8e23a6269b3544ed8f0232e195d0338cab914b65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:34:10 +0900 Subject: [PATCH 066/184] implement M7 optional and nominal result types --- fec/src/types.c | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/fec/src/types.c b/fec/src/types.c index 542df00..af2ff15 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -1,4 +1,5 @@ #include "types.h" +#include "m7.h" #include #include #include @@ -15,6 +16,8 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->kind = kind; t->cname = 0; t->maker = 0; + t->none_cname = 0; + t->unwrap_cname = 0; t->indexer = 0; t->slicer = 0; t->full_slicer = 0; @@ -218,9 +221,11 @@ FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value) if(t->kind==FE_TYPE_UNKNOWN) { t->kind=FE_TYPE_ERROR_UNION; t->error_value=value; + t->drop_cname=generated_name(ctx,"fe_drop_result_","value"); if (value && value->kind != FE_TYPE_VOID) { t->cname=generated_name(ctx,"struct fe_result_","value"); t->maker=generated_name(ctx,"fe_make_result_","value"); + t->none_cname=generated_name(ctx,"fe_fail_result_","value"); t->alloc_cname=generated_name(ctx,"fe_alloc_result_","value"); } } @@ -335,7 +340,7 @@ FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node) if (fc) { t->variants[i].fields = (FeFieldType *)fe_arena_alloc( ctx->arena, fc * sizeof(FeFieldType)); - if (t->variants[i].fields) for (f = v->children; f; f = f->next) + if (t->variants[i].fields) for (f = v->children; f; f=f->next) if (f->kind == FE_N_FIELD) { t->variants[i].fields[j].name = f->text; t->variants[i].fields[j].type = 0; @@ -385,8 +390,6 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) unsigned max_align; if (!t || t->size) return; if (t->cycle_state == 1) { - /* The checker reports this as an invalid by-value cycle. Give the - layout walk a sentinel size so error recovery cannot recurse. */ t->size = 1; t->align = 1; return; @@ -406,6 +409,18 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) } t->cycle_state = 2; return; } + if (t->kind == FE_TYPE_OPTIONAL) { + layout_type(ctx,t->elem); + if (fe_m7_optional_uses_niche(t->elem)) { + t->size=fe_type_size(t->elem); + t->align=fe_type_align(t->elem); + } else { + t->align=ctx->pointer_bits==16 ? 1U : fe_type_align(t->elem); + t->size=round_up(1UL,t->align)+fe_type_size(t->elem); + t->size=round_up(t->size,t->align); + } + t->cycle_state=2; return; + } if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) { t->size = 1; t->align = 1; t->cycle_state = 2; return; } @@ -524,6 +539,8 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) strcmp(node->text,"&mut") == 0); if (node->text && strcmp(node->text,"^")==0) return fe_type_owned(ctx,fe_type_from_ast(ctx,node->a)); + if (node->text && strcmp(node->text,"?")==0) + return fe_m7_optional_type(ctx,fe_type_from_ast(ctx,node->a)); if (node->text && (strcmp(node->text, "[") == 0 || strcmp(node->text, "[]mut") == 0)) { if (node->a) { @@ -535,16 +552,13 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) fe_type_mut_slice(ctx, fe_type_from_ast(ctx,node->b)) : fe_type_slice(ctx, fe_type_from_ast(ctx, node->b)); } - if (node->text && strcmp(node->text, "!") == 0) - /* Prefix !T stores T in a; the E!T spelling stores its success - type in b and the error type in a. */ - return fe_type_error_union(ctx, fe_type_from_ast( - ctx, node->b ? node->b : node->a)); - if (node->text && (strcmp(node->text, "?") == 0 || - strcmp(node->text, "^") == 0 || - strcmp(node->text, "&") == 0 || - strcmp(node->text, "&mut") == 0 || - strcmp(node->text, "*") == 0 || + if (node->text && strcmp(node->text, "!") == 0) { + if (node->b) + return fe_m7_error_union_type(ctx,fe_type_from_ast(ctx,node->a), + fe_type_from_ast(ctx,node->b)); + return fe_type_error_union(ctx,fe_type_from_ast(ctx,node->a)); + } + if (node->text && (strcmp(node->text, "*") == 0 || strcmp(node->text, "far") == 0)) return fe_type_intern(ctx, ""); if (node->text && strcmp(node->text, "fn") == 0) @@ -571,6 +585,8 @@ int fe_type_is_indexable(const FeType *t) const char *fe_type_c_name(const FeType *t, unsigned pointer_bits) { if (!t) return "long"; + if (t->kind == FE_TYPE_OPTIONAL && fe_m7_optional_uses_niche(t->elem)) + return fe_type_c_name(t->elem,pointer_bits); if (t->cname) return t->cname; if (t->kind == FE_TYPE_VOID) return "void"; if (t->kind == FE_TYPE_ERROR_UNION) { From 5be7a5b85203d0bf0f5ce6528a4e04bc4376f1ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:35:11 +0900 Subject: [PATCH 067/184] preserve optional if-let patterns --- fec/src/parser.c | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/fec/src/parser.c b/fec/src/parser.c index 8d8f8f4..37b6f89 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -161,9 +161,6 @@ static FeNode *expr(FeParser *p, int minprec) return left; } -/* A control-flow header is followed by a body '{'. Do not let that body - brace be consumed as the postfix struct-literal brace; callers can use - parentheses when a struct literal is intended in the header. */ static FeNode *header_expr(FeParser *p) { FeNode *n; @@ -238,7 +235,28 @@ static FeNode *statement(FeParser *p) if(eat(p,FE_TOK_LET)) { n=toknode(p,FE_N_LET,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in let");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_VAR)) { n=toknode(p,FE_N_VAR,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected variable name");if(eat(p,FE_TOK_COLON))n->a=type(p);if(eat(p,FE_TOK_EQ))n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } if(eat(p,FE_TOK_CONST)) { n=toknode(p,FE_N_CONST,t);if(is_name(p)){n->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected constant name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in const");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';'");return n; } - if(eat(p,FE_TOK_IF)) { n=toknode(p,FE_N_IF,t);if(eat(p,FE_TOK_LET)){n->text=fe_arena_strdup(&p->ast->arena,"if let",6);if(is_name(p))next(p);if(eat(p,FE_TOK_LPAREN)){if(is_name(p))next(p);want(p,FE_TOK_RPAREN,"expected ')' in if let pattern");}want(p,FE_TOK_EQ,"expected '=' in if let");}n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } + if(eat(p,FE_TOK_IF)) { + n=toknode(p,FE_N_IF,t); + if(eat(p,FE_TOK_LET)) { + FeToken pt=p->current; + n->text=fe_arena_strdup(&p->ast->arena,"if let",6); + if(is_name(p)) { + n->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length); + next(p); + } else error(p,"expected if let pattern"); + if(eat(p,FE_TOK_LPAREN)) { + if(is_name(p)) { + FeNode *binding=toknode(p,FE_N_IDENT,p->current); + next(p); + fe_node_add(n,binding); + } else error(p,"expected if let binding"); + want(p,FE_TOK_RPAREN,"expected ')' in if let pattern"); + } + want(p,FE_TOK_EQ,"expected '=' in if let"); + (void)pt; + } + n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; + } if(eat(p,FE_TOK_COMPTIME)) { n=toknode(p,FE_N_IF,t);want(p,FE_TOK_IF,"expected 'if' after comptime");n->text=fe_arena_strdup(&p->ast->arena,"comptime if",11);n->a=header_expr(p);n->b=block(p);if(eat(p,FE_TOK_ELSE))n->c=is(p,FE_TOK_IF)?statement(p):block(p);return n; } if(eat(p,FE_TOK_WHILE)) {n=toknode(p,FE_N_WHILE,t);n->a=header_expr(p);n->b=block(p);return n;} if(eat(p,FE_TOK_FOR)) {n=toknode(p,FE_N_FOR,t);if(is_name(p)){n->text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected loop variable");if(eat(p,FE_TOK_COMMA)){if(is_name(p)){n->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);}else error(p,"expected second loop variable");}want(p,FE_TOK_IN,"expected 'in' in for");n->a=header_expr(p);if(eat(p,FE_TOK_DOTDOT))n->c=header_expr(p);n->b=block(p);return n;} From e0232a3e920c63e17fb56ffa27ca8125ae277e9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:37:31 +0900 Subject: [PATCH 068/184] extend ownership for M7 wrapper types --- fec/src/own_m7.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 fec/src/own_m7.c diff --git a/fec/src/own_m7.c b/fec/src/own_m7.c new file mode 100644 index 0000000..2a65fd5 --- /dev/null +++ b/fec/src/own_m7.c @@ -0,0 +1,56 @@ +/* M7 extension layer for compiler A. Keep the M6 implementation intact and + replace only the two entry points whose semantics grow for wrapper types. */ +#define fe_own_is_copy_type fe_own_is_copy_type_m6 +#define fe_own_mark_consumed fe_own_mark_consumed_m6 +#include "own.c" +#undef fe_own_is_copy_type +#undef fe_own_mark_consumed + +static int m7_replace_unwrap(FeNode *expr) +{ + FeNode *call; + FeNode *member; + if (!expr || expr->kind != FE_N_MEMBER || !expr->text || + strcmp(expr->text,".?") != 0) + return 0; + call=expr->a; + if (!call || call->kind != FE_N_CALL || !call->a || + call->a->kind != FE_N_MEMBER) + return 0; + member=call->a; + return member->a && member->a->kind==FE_N_IDENT && member->a->text && + strcmp(member->a->text,"mem")==0 && member->b && member->b->text && + strcmp(member->b->text,"replace")==0; +} + +int fe_own_is_copy_type(FeType *type) +{ + if (!type) return 1; + if (type->kind==FE_TYPE_OPTIONAL) + return fe_own_is_copy_type(type->elem); + if (type->kind==FE_TYPE_ERROR_UNION) + return fe_own_is_copy_type(type->error_value); + return fe_own_is_copy_type_m6(type); +} + +void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, + FeNode *expr, FeType *type, int in_defer) +{ + if (!expr || !type || fe_own_is_copy_type(type)) return; + if (expr->kind == FE_N_INDEX && type->kind == FE_TYPE_SLICE && + (expr->c || !expr->b)) + return; + if ((expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) && + !m7_replace_unwrap(expr)) { + fe_diag_error(diags, expr->loc, + "cannot move a non-Copy value out of a projection; use mem.replace"); + return; + } + if (expr->kind != FE_N_IDENT || !state) return; + if (in_defer) { + if (decl) decl->flags |= FE_OWN_NODE_DEFER_CAPTURE; + return; + } + *state = FE_OWN_MOVED; + expr->flags |= FE_OWN_NODE_CONSUMED; +} From b79970241a401b37322d8f1ada20e6f4bae36ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:40:22 +0900 Subject: [PATCH 069/184] extend ownership through M7 wrappers --- fec/src/own.c | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/fec/src/own.c b/fec/src/own.c index ee37c9b..8b51c37 100644 --- a/fec/src/own.c +++ b/fec/src/own.c @@ -24,6 +24,10 @@ int fe_own_is_copy_type(FeType *type) if (type->kind == FE_TYPE_OWNED) return 0; if (type->kind == FE_TYPE_REF || type->kind == FE_TYPE_SLICE) return !type->ref_mut; + if (type->kind == FE_TYPE_OPTIONAL) + return fe_own_is_copy_type(type->elem); + if (type->kind == FE_TYPE_ERROR_UNION) + return fe_own_is_copy_type(type->error_value); if (type->kind == FE_TYPE_ARRAY) return fe_own_is_copy_type(type->elem); if (type->kind == FE_TYPE_STRUCT) { @@ -314,9 +318,6 @@ static int fe_own_live_add(FeOwnLiveness *live, FeNode *decl) FeOwnLastUse *slot; unsigned capacity; const char *cname; - /* This prepass runs before local inference. Recording every local is - harmless (only reference bindings consume the result) and lets an - inferred `let r = &x` participate in last-use analysis. */ if (!live || !decl || !live->arena) return 1; cname = decl->cname ? decl->cname : decl->text; @@ -450,6 +451,23 @@ const FeOwnLastUse *fe_own_last_use(const FeOwnLiveness *live, return 0; } +static int fe_own_replace_unwrap(FeNode *expr) +{ + FeNode *call; + FeNode *member; + if (!expr || expr->kind != FE_N_MEMBER || !expr->text || + strcmp(expr->text,".?") != 0) + return 0; + call=expr->a; + if (!call || call->kind != FE_N_CALL || !call->a || + call->a->kind != FE_N_MEMBER) + return 0; + member=call->a; + return member->a && member->a->kind==FE_N_IDENT && member->a->text && + strcmp(member->a->text,"mem")==0 && member->b && member->b->text && + strcmp(member->b->text,"replace")==0; +} + void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, FeNode *expr, FeType *type, int in_defer) { @@ -457,7 +475,8 @@ void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, if (expr->kind == FE_N_INDEX && type->kind == FE_TYPE_SLICE && (expr->c || !expr->b)) return; - if (expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) { + if ((expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) && + !fe_own_replace_unwrap(expr)) { fe_diag_error(diags, expr->loc, "cannot move a non-Copy value out of a projection; use mem.replace"); return; @@ -470,8 +489,6 @@ void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, } *state = FE_OWN_MOVED; - /* The consuming use owns the live-flag transition. The declaration must - stay live on control-flow paths where the move did not execute. */ expr->flags |= FE_OWN_NODE_CONSUMED; } From c5e2496415ceff4f914ec197f64954e7de479f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:40:31 +0900 Subject: [PATCH 070/184] remove temporary M7 ownership overlay --- fec/src/own_m7.c | 56 ------------------------------------------------ 1 file changed, 56 deletions(-) delete mode 100644 fec/src/own_m7.c diff --git a/fec/src/own_m7.c b/fec/src/own_m7.c deleted file mode 100644 index 2a65fd5..0000000 --- a/fec/src/own_m7.c +++ /dev/null @@ -1,56 +0,0 @@ -/* M7 extension layer for compiler A. Keep the M6 implementation intact and - replace only the two entry points whose semantics grow for wrapper types. */ -#define fe_own_is_copy_type fe_own_is_copy_type_m6 -#define fe_own_mark_consumed fe_own_mark_consumed_m6 -#include "own.c" -#undef fe_own_is_copy_type -#undef fe_own_mark_consumed - -static int m7_replace_unwrap(FeNode *expr) -{ - FeNode *call; - FeNode *member; - if (!expr || expr->kind != FE_N_MEMBER || !expr->text || - strcmp(expr->text,".?") != 0) - return 0; - call=expr->a; - if (!call || call->kind != FE_N_CALL || !call->a || - call->a->kind != FE_N_MEMBER) - return 0; - member=call->a; - return member->a && member->a->kind==FE_N_IDENT && member->a->text && - strcmp(member->a->text,"mem")==0 && member->b && member->b->text && - strcmp(member->b->text,"replace")==0; -} - -int fe_own_is_copy_type(FeType *type) -{ - if (!type) return 1; - if (type->kind==FE_TYPE_OPTIONAL) - return fe_own_is_copy_type(type->elem); - if (type->kind==FE_TYPE_ERROR_UNION) - return fe_own_is_copy_type(type->error_value); - return fe_own_is_copy_type_m6(type); -} - -void fe_own_mark_consumed(FeDiags *diags, int *state, FeNode *decl, - FeNode *expr, FeType *type, int in_defer) -{ - if (!expr || !type || fe_own_is_copy_type(type)) return; - if (expr->kind == FE_N_INDEX && type->kind == FE_TYPE_SLICE && - (expr->c || !expr->b)) - return; - if ((expr->kind == FE_N_MEMBER || expr->kind == FE_N_INDEX) && - !m7_replace_unwrap(expr)) { - fe_diag_error(diags, expr->loc, - "cannot move a non-Copy value out of a projection; use mem.replace"); - return; - } - if (expr->kind != FE_N_IDENT || !state) return; - if (in_defer) { - if (decl) decl->flags |= FE_OWN_NODE_DEFER_CAPTURE; - return; - } - *state = FE_OWN_MOVED; - expr->flags |= FE_OWN_NODE_CONSUMED; -} From a32ad3182a2a4e181d4bc096de3e888976f81f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:42:20 +0900 Subject: [PATCH 071/184] carry contextual M7 type metadata --- fec/src/ast.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fec/src/ast.h b/fec/src/ast.h index 02b3743..efbf887 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -30,6 +30,9 @@ struct FeNode { char *aux_text; char *aux_cname; FeType *sem_type; + /* Expected contextual wrapper, used by M7 for null/Some and E!T + success/failure construction without mutating the expression's type. */ + FeType *sem_context; FeNode *sem_decl; unsigned flags; }; From 369aeb3b868a5dbb505d64e125f7d3c9cdc46d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:42:29 +0900 Subject: [PATCH 072/184] initialize contextual semantic metadata --- fec/src/ast.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/src/ast.c b/fec/src/ast.c index 561b834..7bd93c4 100644 --- a/fec/src/ast.c +++ b/fec/src/ast.c @@ -8,7 +8,7 @@ FeNode *fe_node(FeAst *a, FeNodeKind k, FeLoc loc, const char *text, unsigned lo FeNode *n=(FeNode *)fe_arena_alloc(&a->arena,sizeof(FeNode)); if (!n) return 0; n->kind=k; n->loc=loc; n->text=text?fe_arena_strdup(&a->arena,text,len):0; - n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->aux_text=0; n->aux_cname=0; n->sem_type=0; n->sem_decl=0; n->flags=0; return n; + n->a=n->b=n->c=n->children=n->next=0; n->cname=0; n->aux_text=0; n->aux_cname=0; n->sem_type=0; n->sem_context=0; n->sem_decl=0; n->flags=0; return n; } void fe_node_add(FeNode *parent, FeNode *child) { From 3a7d7de0d332092a93d6c38a1474e19bf307d594 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 21:43:49 +0900 Subject: [PATCH 073/184] test: add unified DOSBox-X milestone runner --- .gitignore | 3 + AGENTS.md | 7 +- pyproject.toml | 2 + src/ferrolang_vm/dosboxx.py | 236 +++++++++++++++++++++++++ src/ferrolang_vm/registry_m1_m3.py | 69 ++++++++ src/ferrolang_vm/registry_m4_m6.py | 49 +++++ src/ferrolang_vm/suite.py | 22 +++ src/ferrolang_vm/test_cli.py | 72 ++++++++ tools/README.md | 32 +++- tools/tests/test_milestones_dosboxx.py | 54 ++++++ tools/toolchains/dosboxx.lock.json | 23 +++ uv.lock | 45 +++++ 12 files changed, 610 insertions(+), 4 deletions(-) create mode 100644 src/ferrolang_vm/dosboxx.py create mode 100644 src/ferrolang_vm/registry_m1_m3.py create mode 100644 src/ferrolang_vm/registry_m4_m6.py create mode 100644 src/ferrolang_vm/suite.py create mode 100644 src/ferrolang_vm/test_cli.py create mode 100644 tools/tests/test_milestones_dosboxx.py create mode 100644 tools/toolchains/dosboxx.lock.json diff --git a/.gitignore b/.gitignore index ac1e67a..e699b19 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ !.qemu/share/*.c .qemu/share/fec/ +# Reproducible DOSBox-X/Open Watcom development cache and ephemeral runs +.dosboxx/ + .qemu/*.qcow2 .qemu/*.img .qemu/*.iso diff --git a/AGENTS.md b/AGENTS.md index 629b919..c9590c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,11 +18,16 @@ VM 자동화 **명령 목록과 플래그는 문서가 아니라 CLI가 규범** ```powershell uv run ferro-vm --help uv run ferro-vm --help +uv run ferro-test --help ``` ## 검증 규칙 -- **실행 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox는 쓰지 않는다. +- 개발 중 빠른 회귀 검사는 `uv run ferro-test run --through `로 + DOSBox-X에서 수행한다. + 이 경로도 컴파일러 A를 DOS 내부 Open Watcom으로 매번 새로 빌드한다. +- **마일스톤의 최종 공식 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox-X 결과는 + 개발용 smoke test이며 완료 게이트를 대체하지 않는다. - 컴파일러 A와 생성 C 모두 VM 안의 Open Watcom으로 컴파일한다. 컴파일러 A와 bits16은 `WCL`, bits32 생성 C는 `WCL386`. - **호스트에서 컴파일하지 않는다.** WSL이나 호스트 C 컴파일러 결과는 정식 검증으로 diff --git a/pyproject.toml b/pyproject.toml index 2660113..9b0a084 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,13 @@ description = "Ferro language compiler and QEMU development automation" requires-python = ">=3.12" dependencies = [ "onnxruntime>=1.28.0", + "pytest>=9.0.0", "rapidocr>=3.9.2", ] [project.scripts] ferro-vm = "ferrolang_vm.cli:main" +ferro-test = "ferrolang_vm.test_cli:main" [build-system] requires = ["hatchling"] diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py new file mode 100644 index 0000000..1deb9c6 --- /dev/null +++ b/src/ferrolang_vm/dosboxx.py @@ -0,0 +1,236 @@ +"""Reproducible DOSBox-X/Open Watcom development test backend.""" +from __future__ import annotations + +import hashlib +import json +import os +import shutil +import subprocess +import tempfile +import urllib.request +import zipfile +from dataclasses import dataclass +from pathlib import Path + +from .daemon import ROOT +from .suite import Case + + +CACHE = ROOT / ".dosboxx" +LOCK_PATH = ROOT / "tools" / "toolchains" / "dosboxx.lock.json" +RUNS = CACHE / "runs" + + +class DosboxError(RuntimeError): + pass + + +def _lock() -> dict[str, object]: + return json.loads(LOCK_PATH.read_text(encoding="utf-8")) + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + while chunk := source.read(1024 * 1024): + digest.update(chunk) + return digest.hexdigest() + + +def _download(name: str, spec: dict[str, object]) -> Path: + downloads = CACHE / "downloads" + downloads.mkdir(parents=True, exist_ok=True) + target = downloads / Path(str(spec["url"])).name + expected = str(spec["sha256"]).lower() + if target.is_file() and _sha256(target) == expected: + return target + target.unlink(missing_ok=True) + partial = target.with_suffix(target.suffix + ".part") + partial.unlink(missing_ok=True) + print(f"ferro-test: downloading {name} {spec['version']}...") + try: + with urllib.request.urlopen(str(spec["url"])) as response, partial.open("wb") as output: + shutil.copyfileobj(response, output, length=1024 * 1024) + except Exception: + partial.unlink(missing_ok=True) + raise + actual = _sha256(partial) + if actual != expected: + partial.unlink(missing_ok=True) + raise DosboxError(f"{name} SHA-256 mismatch: expected {expected}, got {actual}") + partial.replace(target) + return target + + +def _safe_extract(archive: Path, destination: Path) -> None: + destination.parent.mkdir(parents=True, exist_ok=True) + temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}-", dir=destination.parent)) + try: + with zipfile.ZipFile(archive) as bundle: + root = temporary.resolve() + for member in bundle.infolist(): + target = (temporary / member.filename).resolve() + if target != root and root not in target.parents: + raise DosboxError(f"unsafe archive member: {member.filename}") + bundle.extractall(temporary) + if destination.exists(): + shutil.rmtree(destination) + temporary.replace(destination) + except Exception: + shutil.rmtree(temporary, ignore_errors=True) + raise + + +def _required_paths(lock: dict[str, object]) -> tuple[Path, Path, list[Path]]: + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + dosbox = CACHE / "dosbox-x" / str(dosbox_spec["executable"]) + watcom = CACHE / "watcom" + required = [watcom / str(item) for item in watcom_spec["required"]] + return dosbox, watcom, required + + +def setup(*, accept_watcom_license: bool = False) -> tuple[Path, Path]: + if os.name != "nt": + raise DosboxError("the DOSBox-X development backend currently supports Windows only") + lock = _lock() + dosbox, watcom, watcom_required = _required_paths(lock) + marker = CACHE / "SETUP.OK" + lock_hash = _sha256(LOCK_PATH) + if (marker.is_file() and marker.read_text(encoding="ascii").strip() == lock_hash + and dosbox.is_file() and all(path.is_file() for path in watcom_required)): + return dosbox, watcom + if not accept_watcom_license: + raise DosboxError( + "Open Watcom is distributed under the Sybase Open Watcom Public License. " + "Review tools/toolchains/dosboxx.lock.json and rerun " + "`uv run ferro-test setup --accept-watcom-license`." + ) + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") + _safe_extract(_download("Open Watcom", watcom_spec), watcom) + dosbox, watcom, watcom_required = _required_paths(lock) + missing = [str(path.relative_to(CACHE)) for path in [dosbox, *watcom_required] + if not path.is_file()] + if missing: + raise DosboxError("toolchain archive is missing: " + ", ".join(missing)) + marker.write_text(lock_hash + "\n", encoding="ascii") + return dosbox, watcom + + +def resolve_tools() -> tuple[Path, Path]: + lock = _lock() + dosbox, watcom, required = _required_paths(lock) + if not dosbox.is_file() or not all(path.is_file() for path in required): + raise DosboxError("toolchain is not installed; run `uv run ferro-test setup`") + return dosbox, watcom + + +def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: + lines = [ + "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", + "set WATCOM=W:", "set INCLUDE=W:\\H", + "set LIB=W:\\LIB286\\DOS;W:\\LIB286;W:\\LIB386\\DOS;W:\\LIB386", + "call BUILD.BAT", "if not exist BUILD.OK goto BUILDFAIL", + "echo PASS>RESULTS\\BUILD.RES", + ] + for index, case in enumerate(cases): + key = f"C{index:03d}" + command = case.command + if not trace_dos: + command += f" > RESULTS\\{key}.LOG" + lines.append(command) + if case.expect_success: + lines.extend([ + f"if errorlevel 1 goto {key}F", f"echo PASS>RESULTS\\{key}.RES", + f"goto {key}D", f":{key}F", f"echo FAIL>RESULTS\\{key}.RES", f":{key}D", + ]) + else: + lines.extend([ + f"if errorlevel 1 goto {key}P", f"echo FAIL>RESULTS\\{key}.RES", + f"goto {key}D", f":{key}P", f"echo PASS>RESULTS\\{key}.RES", f":{key}D", + ]) + lines.extend([ + "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", + "echo DONE>RUN.OK", *(["pause"] if show_dos else []), "exit", "", + ]) + return "\r\n".join(lines) + + +@dataclass +class SuiteRun: + root: Path + cases: list[Case] + keep: bool = False + + @property + def fec(self) -> Path: + return self.root / "FEC" + + def _key(self, case: Case) -> str: + return f"C{self.cases.index(case):03d}" + + def result(self, case: Case | None = None) -> str: + name = "BUILD" if case is None else self._key(case) + path = self.fec / "RESULTS" / f"{name}.RES" + return path.read_text(encoding="ascii").strip() if path.is_file() else "MISSING" + + def log(self, case: Case | None = None) -> str: + name = "BUILD" if case is None else self._key(case) + path = self.fec / "RESULTS" / f"{name}.LOG" + content = path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" + if content: + return content + errors = sorted(self.fec.glob("*.ERR")) + if errors: + return "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in errors) + console = self.root / "CONSOLE.LOG" + return console.read_text(encoding="utf-8", errors="replace") if console.is_file() else "" + + def cleanup(self) -> None: + if not self.keep: + shutil.rmtree(self.root, ignore_errors=True) + + +def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, + trace_dos: bool = False) -> SuiteRun: + dosbox, watcom = resolve_tools() + RUNS.mkdir(parents=True, exist_ok=True) + run_root = Path(tempfile.mkdtemp(prefix="suite-", dir=RUNS)) + result = SuiteRun(run_root, cases, keep) + fec = result.fec + try: + shutil.copytree(ROOT / "fec" / "src", fec / "SRC") + shutil.copytree(ROOT / "fec" / "std", fec / "STD") + shutil.copytree(ROOT / "fec" / "tests", fec / "TESTS") + shutil.copy2(ROOT / "fec" / "build-dos.bat", fec / "BUILD.BAT") + console = run_root / "CONSOLE.LOG" + config = run_root / "DOSBOX.CON" + config.write_text( + f"[log]\nlogfile={console}\n[dosbox]\nlog console=quiet\n", + encoding="ascii", + ) + (fec / "RUN.BAT").write_text( + _batch(cases, show_dos=show_dos, trace_dos=trace_dos), + encoding="ascii", newline="", + ) + command = [str(dosbox)] + if not show_dos: + command.append("-silent") + command.extend([ + "-fastlaunch", "-conf", str(config), + "-c", f'mount C "{run_root}"', "-c", f'mount W "{watcom}" -ro', + "-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT", + ]) + completed = subprocess.run(command, check=False, timeout=300) + if completed.returncode != 0: + raise DosboxError(f"DOSBox-X exited with status {completed.returncode}") + if not (fec / "RUN.OK").is_file(): + raise DosboxError("DOSBox-X did not complete the test batch") + return result + except Exception: + result.keep = True + raise diff --git a/src/ferrolang_vm/registry_m1_m3.py b/src/ferrolang_vm/registry_m1_m3.py new file mode 100644 index 0000000..a9623ab --- /dev/null +++ b/src/ferrolang_vm/registry_m1_m3.py @@ -0,0 +1,69 @@ +"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" +from __future__ import annotations + +from .suite import Case + + +def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +M1_M3_CASES: list[Case] = [] +for _name in ("basic", "literals", "keybuilt", "v012form"): + M1_M3_CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) +for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): + M1_M3_CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) +for _name in ("misssemi", "unclcomm", "logical"): + M1_M3_CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) + +for _name in ("hello", "scopes"): + _upper = _name.upper() + M1_M3_CASES.extend([ + _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), + ]) +M1_M3_CASES.extend([ + _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), +]) +_m2_outputs = { + "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", + "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", + "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", +} +for _name, _output in _m2_outputs.items(): + M1_M3_CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) + +def _m3_runtime(name: str) -> list[Case]: + upper = name.upper() + return [ + _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), + ] + + +for _name in ("struct", "enum", "array", "mutable"): + M1_M3_CASES.extend(_m3_runtime(_name)) +for _name in ("bad-mlet", "bad-shwr"): + M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) +for _name in ("str", "for", "nested", "char", "arrayctx"): + M1_M3_CASES.extend(_m3_runtime(_name)) +for _name in ("bounds", "slcbound"): + _upper = _name.upper() + M1_M3_CASES.extend([ + _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), + ]) +M1_M3_CASES.extend([ + _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), + _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), +]) +for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): + M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) diff --git a/src/ferrolang_vm/registry_m4_m6.py b/src/ferrolang_vm/registry_m4_m6.py new file mode 100644 index 0000000..48b23e0 --- /dev/null +++ b/src/ferrolang_vm/registry_m4_m6.py @@ -0,0 +1,49 @@ +"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. + +Only commands and their expected status live here; the ``.fe`` fixtures stay +under ``fec/tests`` and are copied by the runner. +""" +from __future__ import annotations + +from .suite import Case + + +def _c(milestone: int, name: str, command: str, ok: bool) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +M4_M6_CASES: list[Case] = [ + _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), + _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), + _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), + _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), + _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), + _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), + _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), +] + +_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} +for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): + _output = _m4_outputs.get(_name, _name.upper()) + M4_M6_CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) + +for _name in ("defer", "owned"): + M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) +for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): + _output = "BAD-DES" if _name == "bad-dest" else _name.upper() + M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) +M4_M6_CASES += [ + _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), + _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), + _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), +] + +for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): + M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) +for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): + M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py new file mode 100644 index 0000000..7072f51 --- /dev/null +++ b/src/ferrolang_vm/suite.py @@ -0,0 +1,22 @@ +"""Shared types and selection for the explicit milestone registries.""" +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class Case: + id: str + milestone: int + command: str + expect_success: bool + + +def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: + from .registry_m1_m3 import M1_M3_CASES + from .registry_m4_m6 import M4_M6_CASES + + cases = [*M1_M3_CASES, *M4_M6_CASES] + if only is not None: + return [case for case in cases if case.milestone == only] + return [case for case in cases if case.milestone <= through] diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py new file mode 100644 index 0000000..8a9d1e3 --- /dev/null +++ b/src/ferrolang_vm/test_cli.py @@ -0,0 +1,72 @@ +"""Developer test entry point backed by a disposable DOSBox-X run.""" +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +from .dosboxx import DosboxError, setup + + +MILESTONES = tuple(f"m{number}" for number in range(1, 7)) + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="ferro-test", + description="Fast Ferro development tests in DOSBox-X/Open Watcom.", + ) + commands = parser.add_subparsers(dest="command", required=True) + prepare = commands.add_parser("setup", help="download and verify pinned development tools") + prepare.add_argument("--accept-watcom-license", action="store_true", + help="confirm acceptance of the Sybase Open Watcom Public License") + run = commands.add_parser("run", help="build once and run milestone pytest cases") + selection = run.add_mutually_exclusive_group() + selection.add_argument("--through", choices=MILESTONES, default="m6", + help="run cumulatively through this milestone (default: m6)") + selection.add_argument("--only", choices=MILESTONES, + help="run only this milestone's cases") + run.add_argument("-v", "--verbose", action="store_true", help="show every pytest case") + run.add_argument("--keep-failed", action="store_true", + help="keep the disposable DOS filesystem after failures") + run.add_argument("--show-dos", action="store_true", + help="show DOSBox-X and wait for a key before closing") + run.add_argument("--dos-log", action="store_true", + help="print the captured DOS console after the run") + run.add_argument("--trace-dos", action="store_true", + help="do not redirect case command output") + args = parser.parse_args() + try: + if args.command == "setup": + dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) + print(f"DOSBox-X: {dosbox}") + print(f"Open Watcom: {watcom}") + return 0 + if args.only: + os.environ["FERRO_TEST_ONLY"] = args.only + else: + os.environ["FERRO_TEST_THROUGH"] = args.through + for enabled, name in ( + (args.keep_failed, "FERRO_TEST_KEEP_FAILED"), + (args.show_dos, "FERRO_TEST_SHOW_DOS"), + (args.trace_dos, "FERRO_TEST_TRACE_DOS"), + (args.dos_log, "FERRO_TEST_DOS_LOG"), + ): + if enabled: + os.environ[name] = "1" + import pytest + test_file = os.fspath( + Path(__file__).resolve().parents[2] / "tools" / "tests" / "test_milestones_dosboxx.py" + ) + pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"] + if args.dos_log: + pytest_args.append("-s") + return int(pytest.main(pytest_args)) + except (DosboxError, ValueError) as exc: + print(f"ferro-test: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/README.md b/tools/README.md index 3d909a5..2e3429e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,12 +2,37 @@ ## Host support -Automation currently supports **Windows 10/11 only**. It requires `uv`, QEMU -with WHPX support, and `ffmpeg.exe` on `PATH`. The Python implementation uses -portable APIs where possible, but other hosts are not supported yet. +Automation currently supports **Windows 10/11 only**. The fast development loop +requires only `uv`; it downloads pinned DOSBox-X and Open Watcom DOS releases. +The final milestone gate additionally requires QEMU with WHPX support and +`ffmpeg.exe` on `PATH`. Other hosts are not supported yet. ## Getting started +```powershell +uv run ferro-test setup --accept-watcom-license +uv run ferro-test run --through m6 -v +``` + +`setup` reads `tools/toolchains/dosboxx.lock.json`, downloads the exact official +archives, verifies their SHA-256 hashes, and extracts them under ignored +`.dosboxx/`. Review the Open Watcom license referenced by the lock file before +accepting it. Archives and installed tools are deliberately not committed. + +Each `run` creates a disposable DOS drive, copies the current compiler, standard +library, and fixtures, then builds `FEC.EXE` once inside DOS with Open Watcom. +All selected milestone commands execute sequentially in that same DOSBox-X +instance, while pytest reports every emit, Watcom build, runtime, and rejection +check separately. Thus stale QEMU binaries cannot make the test pass. + +`--through m6` runs cumulatively from M1; `--only m6` selects one milestone. +Use `--keep-failed` to preserve a failed drive under `.dosboxx/runs/`, +`--dos-log` to print the captured DOS console, `--trace-dos` to disable command +output redirection, and `--show-dos` to keep the GUI open until a key is pressed. + +This is the quick development smoke test. Run the QEMU/FreeDOS workflow below +for the authoritative milestone completion gate. + ```powershell uv run ferro-vm start uv run ferro-vm status @@ -18,6 +43,7 @@ The command list lives in the CLI itself, not in this file: ```powershell uv run ferro-vm --help uv run ferro-vm --help +uv run ferro-test --help ``` Working rules, verification gates, and DOS build traps are in `AGENTS.md`. diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py new file mode 100644 index 0000000..c6bc2fd --- /dev/null +++ b/tools/tests/test_milestones_dosboxx.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import os + +import pytest + +from ferrolang_vm.dosboxx import SuiteRun, run_suite +from ferrolang_vm.suite import Case, all_cases + + +def _number(name: str) -> int: + if not name.startswith("m") or not name[1:].isdigit(): + raise ValueError(f"invalid milestone: {name}") + value = int(name[1:]) + if value not in range(1, 7): + raise ValueError(f"unsupported milestone: {name}") + return value + + +ONLY = os.environ.get("FERRO_TEST_ONLY") +CASES = all_cases( + through=_number(os.environ.get("FERRO_TEST_THROUGH", "m6")), + only=_number(ONLY) if ONLY else None, +) + + +@pytest.fixture(scope="session") +def suite_run() -> SuiteRun: + run = run_suite( + CASES, + keep=os.environ.get("FERRO_TEST_KEEP_FAILED") == "1", + show_dos=os.environ.get("FERRO_TEST_SHOW_DOS") == "1", + trace_dos=os.environ.get("FERRO_TEST_TRACE_DOS") == "1", + ) + yield run + if os.environ.get("FERRO_TEST_DOS_LOG") == "1": + console = run.root / "CONSOLE.LOG" + if console.is_file(): + print(console.read_text(encoding="utf-8", errors="replace")) + run.cleanup() + + +def test_compiler_build(suite_run: SuiteRun) -> None: + assert suite_run.result() == "PASS", suite_run.log() + + +@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id) +def test_milestone_case(case: Case, suite_run: SuiteRun) -> None: + if suite_run.result() != "PASS": + pytest.skip("compiler build failed") + assert suite_run.result(case) == "PASS", ( + f"DOS command: {case.command}\nExpected success: {case.expect_success}\n" + f"{suite_run.log(case)}" + ) diff --git a/tools/toolchains/dosboxx.lock.json b/tools/toolchains/dosboxx.lock.json new file mode 100644 index 0000000..08b1123 --- /dev/null +++ b/tools/toolchains/dosboxx.lock.json @@ -0,0 +1,23 @@ +{ + "schema": 1, + "dosboxx": { + "version": "2026.08.02", + "url": "https://github.com/joncampbell123/dosbox-x/releases/download/dosbox-x-v2026.08.02/dosbox-x-vsbuild-win64-2026.08.02-portable.zip", + "sha256": "ca28208f5fee25a74caf3a02cc0189c7f7943a42ce37c02848f5fc03450a96cf", + "executable": "bin/x64/Release/dosbox-x.exe" + }, + "open_watcom": { + "version": "2026-08-01-Build", + "url": "https://github.com/open-watcom/open-watcom-v2/releases/download/2026-08-01-Build/open-watcom-2_0-c-dos.exe", + "sha256": "80db4ab340f382e59bf3d396280576ec837964c2ef00e8ddd3b2b3724ab63edf", + "required": [ + "binw/wcl.exe", + "binw/wcl386.exe", + "binp/wlink.exe", + "h/stdio.h", + "lib286/dos/clibl.lib", + "lib386/dos/clib3r.lib", + "license.txt" + ] + } +} diff --git a/uv.lock b/uv.lock index c8f60c6..c7b989e 100644 --- a/uv.lock +++ b/uv.lock @@ -175,12 +175,14 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "onnxruntime" }, + { name = "pytest" }, { name = "rapidocr" }, ] [package.metadata] requires-dist = [ { name = "onnxruntime", specifier = ">=1.28.0" }, + { name = "pytest", specifier = ">=9.0.0" }, { name = "rapidocr", specifier = ">=3.9.2" }, ] @@ -201,6 +203,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + [[package]] name = "numpy" version = "2.5.2" @@ -418,6 +429,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + [[package]] name = "protobuf" version = "7.35.1" @@ -463,6 +483,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608 }, ] +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, +] + [[package]] name = "pyyaml" version = "6.0.3" From 1e90eba1bedddd345944cd80b762598acdd94fe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:44:46 +0900 Subject: [PATCH 074/184] implement M7 semantic checker --- fec/src/check_m7.c | 1167 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1167 insertions(+) create mode 100644 fec/src/check_m7.c diff --git a/fec/src/check_m7.c b/fec/src/check_m7.c new file mode 100644 index 0000000..5af921f --- /dev/null +++ b/fec/src/check_m7.c @@ -0,0 +1,1167 @@ +/* Compiler-A M7 integration. The verified M1-M6 checker remains the exact + fast path for sources that do not use M7 syntax or types. M7 sources reuse + its symbol, ownership and flow helpers from this translation unit. */ +#define fe_check_init fe_check_init_m6 +#define fe_check_program fe_check_program_m6 +#define fe_check_expr_type fe_check_expr_type_m6 +#include "check.c" +#undef fe_check_init +#undef fe_check_program +#undef fe_check_expr_type + +#include "m7.h" +#include + +#define FE_M7_FLOW_CAP 64U + +static FeType *m7_check_expr(FeCheckerState *s, FeNode *n); +static void m7_check_stmt(FeCheckerState *s, FeNode *n); + +static int m7_type_ast(const FeNode *n) +{ + const FeNode *x; + if (!n) return 0; + if (n->kind==FE_N_TYPE && n->text && + (strcmp(n->text,"?")==0 || strcmp(n->text,"!")==0)) + return 1; + if (m7_type_ast(n->a) || m7_type_ast(n->b) || m7_type_ast(n->c)) + return 1; + for (x=n->children;x;x=x->next) + if (m7_type_ast(x)) return 1; + return 0; +} + +static int m7_node_feature(const FeNode *n) +{ + const FeNode *x; + if (!n) return 0; + if (n->kind==FE_N_ERROR_DECL) return 1; + if (fe_m7_is_null(n) || fe_m7_is_try(n)) return 1; + if (n->kind==FE_N_MEMBER && n->text && strcmp(n->text,".?")==0) + return 1; + if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) + return 1; + if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) + return 1; + if (n->kind==FE_N_ARM && n->text && + (strcmp(n->text,"Some")==0 || strcmp(n->text,"None")==0)) + return 1; + if (m7_type_ast(n)) return 1; + if (m7_node_feature(n->a) || m7_node_feature(n->b) || + m7_node_feature(n->c)) return 1; + for (x=n->children;x;x=x->next) + if (m7_node_feature(x)) return 1; + return 0; +} + +static int m7_program_feature(FeCheck *c) +{ + return c && c->ast && m7_node_feature(c->ast->root); +} + +static int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) +{ + if (fe_type_equal(want,got)) return 1; + return compatible(want,got,value); +} + +static FeType *m7_check_expected(FeCheckerState *s, FeNode *value, + FeType *expected) +{ + FeType *actual; + FeM7ContextKind context; + if (!value) return unknown(s->c); + if (fe_m7_is_null(value)) { + if (!fe_m7_can_contextual_null(expected)) { + err(s->c,value->loc,"null requires a contextual optional type"); + value->sem_type=unknown(s->c); + return value->sem_type; + } + value->sem_type=expected; + value->sem_context=expected; + return expected; + } + actual=m7_check_expr(s,value); + if (!expected) return actual; + if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && + m7_actual_compatible(expected->elem,actual,value)) { + value->sem_context=expected; + return expected; + } + if (expected->kind==FE_TYPE_ERROR_UNION) { + context=fe_m7_error_context(&s->c->types,expected,actual); + if (context!=FE_M7_CONTEXT_NONE) { + value->sem_context=expected; + return expected; + } + } + return actual; +} + +static int m7_error_same(FeCheckerState *s, FeType *a, FeType *b) +{ + FeType *ea; + FeType *eb; + if (!a || !b || a->kind!=FE_TYPE_ERROR_UNION || + b->kind!=FE_TYPE_ERROR_UNION) return 0; + ea=fe_m7_error_type(&s->c->types,a); + eb=fe_m7_error_type(&s->c->types,b); + return ea && eb && fe_type_equal(ea,eb); +} + +static FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) +{ + FeFieldType *field; + FeType *owner; + if (!base) return unknown(s->c); + if (n->text && strcmp(n->text,".?")==0) { + if (base->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional projection '.?' requires an optional value"); + return unknown(s->c); + } + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"unknown struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + if (base->kind==FE_TYPE_ENUM && n->b && n->b->text) { + if (!fe_type_variant(base,n->b->text)) + err(s->c,n->loc,"unknown enum variant"); + n->sem_type=base; + return base; + } + if ((base->kind==FE_TYPE_SLICE || base->kind==FE_TYPE_STR) && + n->b && n->b->text && strcmp(n->b->text,"n")==0) { + n->sem_type=fe_type_intern(&s->c->types,"usize"); + return n->sem_type; + } + n->sem_type=unknown(s->c); + return n->sem_type; +} + +static int m7_place_is_projection(FeNode *n) +{ + return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); +} + +static FeType *m7_check_call(FeCheckerState *s, FeNode *n) +{ + FeCheck *c; + FeNode *arg; + FeNode *value; + FeNode *param; + FeSym *sym; + FeType *a; + FeType *b; + FeType *expected; + c=s->c; + if (n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"Some")==0) { + err(c,n->loc,"Some is only valid as an optional pattern"); + n->sem_type=unknown(c); + return n->sem_type; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { + arg=n->children; + if (strcmp(n->a->b->text,"replace")==0) { + value=arg ? arg->next : 0; + if (!arg || !value || value->next) { + err(c,n->loc,"mem.replace requires destination and value"); + n->sem_type=unknown(c); + return n->sem_type; + } + a=m7_check_expr(s,arg); + if (!a || a->kind!=FE_TYPE_REF || !a->ref_mut || + !arg->a || !lvalue_writable(s,arg->a)) + err(c,n->loc,"mem.replace destination must be a mutable place"); + expected=a && a->kind==FE_TYPE_REF ? a->elem : 0; + b=m7_check_expected(s,value,expected); + if (expected && !fe_type_equal(expected,b) && + !m7_actual_compatible(expected,b,value)) + err(c,value->loc,"mem.replace value type mismatch"); + mark_moved(s,value,value->sem_type ? value->sem_type : b); + n->sem_type=expected ? expected : unknown(c); + fe_type_require_replace(&c->types,n->sem_type); + return n->sem_type; + } + if (strcmp(n->a->b->text,"destroy")==0) { + a=arg ? m7_check_expr(s,arg) : unknown(c); + if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) + err(c,n->loc,"mem.destroy requires exactly one owned pointer"); + else mark_moved(s,arg,a); + n->sem_type=fe_type_intern(&c->types,"void"); + return n->sem_type; + } + if (strcmp(n->a->b->text,"create")==0 || + strcmp(n->a->b->text,"alloc_slice")==0) + return check_expr(s,n); + } + if (n->a && n->a->kind==FE_N_IDENT) { + sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); + if (!sym || !sym->fn) { + err(c,n->loc,"unknown function"); + n->sem_type=unknown(c); + return n->sem_type; + } + n->a->cname=sym->cname; + n->sem_decl=sym->fn; + param=sym->fn->a ? sym->fn->a->children : 0; + arg=n->children; + while (param && arg) { + b=node_type(c,param->a); + a=m7_check_expected(s,arg,b); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root; + root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (!(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut)) + mark_moved(s,arg,arg->sem_type ? arg->sem_type : a); + if (!fe_type_equal(b,a) && !m7_actual_compatible(b,a,arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind!=FE_TYPE_UNKNOWN) + err(c,arg->loc,"argument type mismatch"); + own_release_temporary_borrow(s,arg); + param=param->next; + arg=arg->next; + } + if (param || arg) err(c,n->loc,"wrong number of arguments"); + n->sem_type=sym->fn->b ? node_type(c,sym->fn->b) : + fe_type_intern(&c->types,"void"); + return n->sem_type; + } + return check_expr(s,n); +} + +static void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, + FeOwnState **own, FeFlowBorrow **borrow, + unsigned *count) +{ + *count=flow_capture(s->scope,slots,FE_M7_FLOW_CAP); + *own=flow_own_new(s,*count); + *borrow=flow_borrow_new(s,*count); + flow_own_capture(slots,*own,*count); + flow_borrow_capture(slots,*borrow,*count); +} + +static void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, + FeFlowBorrow *borrow, unsigned count) +{ + flow_restore(slots,count); + flow_own_restore(slots,own,count); + flow_borrow_restore(slots,borrow,count); +} + +static void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, + FeOwnState *own_base, + FeFlowBorrow *borrow_base, + unsigned count, FeFlowSlot *rhs, + FeOwnState *own_rhs, + FeFlowBorrow *borrow_rhs) +{ + (void)s; + flow_merge(base,base,rhs,count); + flow_own_merge(base,own_base,own_rhs,count); + flow_borrow_merge(base,borrow_base,borrow_rhs,count); +} + +static int m7_stmt_definitely_exits(FeNode *n) +{ + FeNode *last; + if (!n) return 0; + if (n->kind==FE_N_RETURN || n->kind==FE_N_BREAK || + n->kind==FE_N_CONTINUE) return 1; + if (n->kind==FE_N_BLOCK) { + last=n->children; + if (!last) return 0; + while (last->next) last=last->next; + return m7_stmt_definitely_exits(last); + } + if (n->kind==FE_N_IF && n->b && n->c) + return m7_stmt_definitely_exits(n->b) && + m7_stmt_definitely_exits(n->c); + return 0; +} + +static FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, + FeM7LazyKind kind) +{ + FeType *left_type; + FeType *payload; + FeType *right_type; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot rhs[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_rhs; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_rhs; + unsigned count; + unsigned rhs_count; + FeScope *old; + FeType *error_type; + left_type=m7_check_expr(s,n->a); + if (kind==FE_M7_LAZY_ORELSE) { + if (!left_type || left_type->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"orelse requires an optional left operand"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->elem; + if (!fe_own_is_copy_type(payload)) { + if (m7_place_is_projection(n->a)) + err(s->c,n->loc, + "non-Copy optional projection requires mem.replace before orelse"); + else + mark_moved(s,n->a,left_type); + } + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"orelse fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; + } + if (!left_type || left_type->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"catch requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->error_value; + mark_moved(s,n->a,left_type); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + if (n->c) { + old=s->scope; + s->scope=scope_new(s,old); + error_type=fe_m7_error_type(&s->c->types,left_type); + if (n->b && n->b->text) + add_symbol(s,s->scope,n->b->text,error_type,0,0,1, + local_cname(s->c,n->b->text),n->b); + m7_check_stmt(s,n->c); + s->scope=old; + if (payload && payload->kind!=FE_TYPE_VOID && + !m7_stmt_definitely_exits(n->c)) + err(s->c,n->loc, + "catch block for a value result must exit instead of falling through"); + if (payload && payload->kind==FE_TYPE_VOID) { + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + } else { + m7_restore_flow(base,own_base,borrow_base,count); + } + n->sem_type=payload; + return payload; + } + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"catch fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; +} + +static FeType *m7_check_expr(FeCheckerState *s, FeNode *n) +{ + FeType *a; + FeType *b; + FeType *ret_error; + FeType *got_error; + FeM7LazyKind lazy; + const char *op; + if (!n) return unknown(s->c); + if (fe_m7_is_null(n)) { + err(s->c,n->loc,"null requires a contextual optional type"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (n->kind==FE_N_IDENT) + return check_identifier(s,n,1); + if (n->kind==FE_N_LITERAL) + return check_expr(s,n); + if (n->kind==FE_N_CALL) + return m7_check_call(s,n); + if (n->kind==FE_N_MEMBER) { + a=m7_check_expr(s,n->a); + return m7_member_field(s,n,a); + } + if (n->kind==FE_N_INDEX) + return check_index(s,n); + if (n->kind==FE_N_UNARY) { + op=n->text ? n->text : ""; + if (strcmp(op,"try")==0) { + a=m7_check_expr(s,n->a); + if (!a || a->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an enclosing error result"); + } else { + ret_error=fe_m7_error_type(&s->c->types,s->ret); + got_error=fe_m7_error_type(&s->c->types,a); + if (!ret_error || !got_error || !fe_type_equal(ret_error,got_error)) + err(s->c,n->loc, + "try error type must exactly match the enclosing error result"); + } + mark_moved(s,n->a,a); + n->sem_type=a->error_value; + return n->sem_type; + } + if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + a=m7_check_expr(s,n->a); + own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); + n->sem_type=fe_type_ref(&s->c->types,a,strcmp(op,"&mut")==0); + return n->sem_type; + } + return check_expr(s,n); + } + if (n->kind==FE_N_BINARY) { + lazy=fe_m7_lazy_kind(n); + if (lazy!=FE_M7_LAZY_NONE) + return m7_check_lazy(s,n,lazy); + op=n->text ? n->text : ""; + if ((strcmp(op,"==")==0 || strcmp(op,"!=")==0) && + (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { + FeNode *nonnull; + FeNode *nullnode; + nonnull=fe_m7_is_null(n->a) ? n->b : n->a; + nullnode=fe_m7_is_null(n->a) ? n->a : n->b; + a=m7_check_expr(s,nonnull); + if (!a || a->kind!=FE_TYPE_OPTIONAL) + err(s->c,n->loc,"null comparison requires an optional value"); + else { + nullnode->sem_type=a; + nullnode->sem_context=a; + } + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + a=m7_check_expr(s,n->a); + b=m7_check_expr(s,n->b); + if (strcmp(op,"and")==0 || strcmp(op,"or")==0) { + if ((known(a) && a->kind!=FE_TYPE_BOOL) || + (known(b) && b->kind!=FE_TYPE_BOOL)) + err(s->c,n->loc,"logical operator requires bool operands"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + if (strcmp(op,"==")==0 || strcmp(op,"!=")==0 || + strcmp(op,"<")==0 || strcmp(op,"<=")==0 || + strcmp(op,">")==0 || strcmp(op,">=")==0) { + if (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a)) + err(s->c,n->loc,"comparison operands have different types"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b)) || + (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a))) + err(s->c,n->loc,"arithmetic operands must have the same integer type"); + n->sem_type=a; + return a; + } + if (n->kind==FE_N_TYPE && n->text && strcmp(n->text,"as")==0) + return check_expr(s,n); + if (n->kind==FE_N_STRUCT_INIT) + return check_struct_init(s,n); + if (n->kind==FE_N_ARRAY_INIT) + return check_array_init(s,n); + return check_expr(s,n); +} + +static FeType *m7_check_lvalue(FeCheckerState *s, FeNode *n, int read) +{ + FeType *base; + FeFieldType *field; + FeType *owner; + if (!n) return unknown(s->c); + if (n->kind==FE_N_MEMBER) { + base=m7_check_expr(s,n->a); + if (base && base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base && base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + if (!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + n->sem_type=base->elem; + return base->elem; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + if (base->kind==FE_TYPE_REF && !base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"assignment requires a valid struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + } + return check_lvalue(s,n,read); +} + +static FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, + FeNode *source, int *borrow_mut) +{ + FeSym *root; + int mutable; + *borrow_mut=0; + if (fe_own_is_copy_type(payload)) return payload; + root=own_root_symbol(s,source); + mutable=root && root->mutable; + *borrow_mut=mutable; + if (payload->kind==FE_TYPE_OWNED && payload->elem) + return fe_type_ref(&s->c->types,payload->elem,mutable); + return fe_type_ref(&s->c->types,payload,mutable); +} + +static void m7_check_if_let(FeCheckerState *s, FeNode *n) +{ + FeType *opt; + FeType *binding_type; + FeNode *binding; + FeSym *root; + FeScope *old; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + int borrow_mut; + int is_some; + opt=m7_check_expr(s,n->a); + if (!opt || opt->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"if let Some/None requires an optional value"); + return; + } + is_some=n->aux_text && strcmp(n->aux_text,"Some")==0; + if (!is_some && (!n->aux_text || strcmp(n->aux_text,"None")!=0)) + err(s->c,n->loc,"if let optional pattern must be Some or None"); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + binding=n->children; + if (is_some && binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + n->loc); + } + } + m7_check_stmt(s,n->b); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + m7_restore_flow(base,own_base,borrow_base,count); + if (n->c) m7_check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + } else { + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + for (i=0;ichildren;arm;arm=arm->next) { + m7_restore_flow(base,own_base,borrow_base,count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + if (arm->text && strcmp(arm->text,"Some")==0) { + if (seen_some) err(s->c,arm->loc,"duplicate Some match arm"); + seen_some=1; + binding=arm->children; + if (binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + arm->loc); + } + } + } else if (arm->text && strcmp(arm->text,"None")==0) { + if (seen_none) err(s->c,arm->loc,"duplicate None match arm"); + seen_none=1; + } else if (arm->text && strcmp(arm->text,"_")==0) { + wildcard=1; + } else { + err(s->c,arm->loc,"optional match arm must be Some, None, or _"); + } + if (arm->a && arm->a->kind==FE_N_BLOCK) m7_check_stmt(s,arm->a); + else if (arm->a) m7_check_expr(s,arm->a); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,current,count); + own_current=flow_own_new(s,count); + borrow_current=flow_borrow_new(s,count); + flow_own_capture(current,own_current,count); + flow_borrow_capture(current,borrow_current,count); + if (!have) { + for (i=0;ic,n->loc,"non-exhaustive optional match"); + if (have) m7_restore_flow(merged,own_merged,borrow_merged,count); +} + +static void m7_check_match_stmt(FeCheckerState *s, FeNode *n) +{ + FeType *value; + value=m7_check_expr(s,n->a); + if (value && value->kind==FE_TYPE_OPTIONAL) { + m7_check_optional_match(s,n,value); + n->sem_type=unknown(s->c); + return; + } + check_match(s,n); +} + +static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) +{ + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + int initialized; + expected=n->a ? node_type(s->c,n->a) : 0; + if (n->b) + stored=m7_check_expected(s,n->b,expected); + else + stored=expected ? expected : unknown(s->c); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!expected) expected=stored; + if (!n->a && n->b && fe_m7_is_null(n->b)) + err(s->c,n->loc,"null initializer requires an explicit optional type"); + if (expected && expected->kind==FE_TYPE_VOID) + err(s->c,n->loc,"variable cannot have void type"); + if (n->b && !fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) + err(s->c,n->loc,"initializer type mismatch"); + if (n->b) mark_moved(s,n->b,actual); + initialized=n->b!=0; + sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, + local_cname(s->c,n->text ? n->text : "local"),n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth!=0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); +} + +static void m7_check_stmt(FeCheckerState *s, FeNode *n) +{ + FeScope *old; + FeNode *x; + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + old=s->scope; + s->scope=scope_new(s,old); + for (x=n->children;x;x=x->next) { + m7_check_stmt(s,x); + own_release_after_stmt(s,s->scope,x,0); + } + own_release_after_stmt(s,s->scope,n,1); + s->scope=old; + break; + case FE_N_LET: + case FE_N_CONST: + m7_check_decl_stmt(s,n,0); + break; + case FE_N_VAR: + if (!n->a && !n->b) + err(s->c,n->loc,"uninitialized var requires an explicit type"); + m7_check_decl_stmt(s,n,1); + break; + case FE_N_ASSIGN: + expected=m7_check_lvalue(s,n->a,compound_operator(n->text)); + stored=m7_check_expected(s,n->b,expected); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) + err(s->c,n->loc,"assignment type mismatch"); + mark_moved(s,n->b,actual); + sym=n->a && n->a->kind==FE_N_IDENT ? + find_symbol(s->scope,n->a->text) : 0; + if (sym && sym->mutable) { + sym->initialized=1; + fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); + sym->moved=sym->own.move; + } + break; + case FE_N_EXPR_STMT: + m7_check_expr(s,n->a); + break; + case FE_N_DEFER: + ++s->defer_depth; + m7_check_stmt(s,n->a); + --s->defer_depth; + break; + case FE_N_IF: + if (n->text && strcmp(n->text,"if let")==0) + m7_check_if_let(s,n); + else { + FeType *cond; + cond=m7_check_expr(s,n->a); + if (known(cond) && cond->kind!=FE_TYPE_BOOL) + err(s->c,n->loc,"if condition must be bool"); + /* Reuse the verified branch merge by letting the M6 statement + machinery handle branches that contain no M7-only nodes. */ + if (!m7_node_feature(n->b) && !m7_node_feature(n->c)) + check_stmt(s,n); + else { + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + m7_check_stmt(s,n->b); + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + m7_restore_flow(base,own_base,borrow_base,count); + if (n->c) m7_check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + } else { + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + for (i=0;iret; + if (n->a) + stored=m7_check_expected(s,n->a,expected); + else + stored=fe_type_intern(&s->c->types,"void"); + actual=n->a && n->a->sem_type ? n->a->sem_type : stored; + if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a && + actual && actual->kind==FE_TYPE_ERROR_UNION && + !fe_type_equal(expected,actual)) + err(s->c,n->loc,"error result type mismatch"); + else if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->a)) + err(s->c,n->loc,"return type mismatch"); + if (n->a) mark_moved(s,n->a,actual); + break; + case FE_N_WHILE: + case FE_N_FOR: + /* M7 fixtures only need existing loop semantics; any M7 expression in + a loop is still recursively checked by function-local expressions + used in the body through the fallback path below. */ + if (!m7_node_feature(n)) check_stmt(s,n); + else { + if (n->kind==FE_N_WHILE) { + actual=m7_check_expr(s,n->a); + if (known(actual) && actual->kind!=FE_TYPE_BOOL) + err(s->c,n->loc,"while condition must be bool"); + ++s->loop_depth; + m7_check_stmt(s,n->b); + --s->loop_depth; + } else check_for(s,n); + } + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (!s->loop_depth) + err(s->c,n->loc,"break or continue outside loop"); + break; + case FE_N_UNSAFE: + m7_check_stmt(s,n->a); + break; + default: + check_stmt(s,n); + break; + } +} + +static int m7_ast_reference_storage(FeNode *type) +{ + if (!type || !type->text) return 0; + if (strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || + (strcmp(type->text,"[")==0 && !type->a) || + strcmp(type->text,"str")==0) + return 1; + if (strcmp(type->text,"?")==0) + return m7_ast_reference_storage(type->a); + if (strcmp(type->text,"^")==0) return 0; + return 0; +} + +static void m7_check_storage(FeCheck *c, FeNode *decl) +{ + FeNode *m; + if (!decl) return; + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + for (m=decl->children;m;m=m->next) + if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a)) + err(c,m->loc,"reference type is not allowed in aggregate storage"); + } + check_reference_storage(c,decl); +} + +static void m7_validate_error_decl(FeCheck *c, FeNode *decl) +{ + FeNode *a; + FeNode *b; + unsigned long code; + unsigned long other; + if (!decl || decl->kind!=FE_N_ERROR_DECL) return; + for (a=decl->children;a;a=a->next) { + if (!a->a || a->a->kind!=FE_N_LITERAL || !a->a->text) continue; + code=strtoul(a->a->text,0,0); + if (code==0UL) + err(c,a->loc,"error code 0 is reserved for success"); + for (b=decl->children;b && b!=a;b=b->next) { + if (a->text && b->text && strcmp(a->text,b->text)==0) { + err(c,a->loc,"duplicate error member name"); + break; + } + if (b->a && b->a->kind==FE_N_LITERAL && b->a->text) { + other=strtoul(b->a->text,0,0); + if (other==code) { + err(c,a->loc,"duplicate error numeric code"); + break; + } + } + } + } +} + +static void m7_check_fn(FeCheck *c, FeNode *fn, FeScope *globals) +{ + FeCheckerState s; + FeNode *x; + FeType *t; + s.c=c; + s.globals=globals; + s.scope=scope_new(&s,globals); + s.ret=fn->b ? node_type(c,fn->b) : fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->ast->arena); + fe_own_collect_last_uses(&s.liveness,fn); + fn->sem_type=s.ret; + for (x=fn->a ? fn->a->children : 0;x;x=x->next) { + t=node_type(c,x->a); + if (t->kind==FE_TYPE_VOID) + err(c,x->loc,"parameter cannot have void type"); + add_symbol(&s,s.scope,x->text,t,0,1,1, + local_cname(c,x->text ? x->text : "arg"),x); + } + if (fn->c) m7_check_stmt(&s,fn->c); +} + +static void m7_check_method(FeCheck *c, FeNode *fn, FeScope *globals, + FeType *owner) +{ + FeCheckerState s; + FeNode *x; + FeType *t; + s.c=c; + s.globals=globals; + s.scope=scope_new(&s,globals); + s.ret=fn->b ? method_type(c,fn->b,owner) : + fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->ast->arena); + fe_own_collect_last_uses(&s.liveness,fn); + fn->sem_type=s.ret; + for (x=fn->a ? fn->a->children : 0;x;x=x->next) { + t=method_type(c,x->a,owner); + x->sem_type=t; + add_symbol(&s,s.scope,x->text,t,0,1,1, + local_cname(c,x->text ? x->text : "arg"),x); + } + if (fn->c) m7_check_stmt(&s,fn->c); +} + +void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, + unsigned pointer_bits, int no_checks) +{ + fe_check_init_m6(c,ast,diags,pointer_bits,no_checks); +} + +int fe_check_program(FeCheck *c) +{ + FeCheckerState s; + FeNode *n; + FeNode *m; + FeSym *sym; + FeType *t; + FeType *iv; + char method_name[128]; + if (!m7_program_feature(c)) return fe_check_program_m6(c); + s.c=c; + s.scope=scope_new(&s,0); + s.globals=s.scope; + s.ret=fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=0; + fe_own_liveness_init(&s.liveness,&c->ast->arena); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) + fe_type_declare_struct(&c->types,n,(n->flags & 1U)!=0); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + m7_check_storage(c,n); + if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ENUM) fe_type_declare_enum(&c->types,n); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); + check_type_cycles(c); + fe_type_layout_all(&c->types); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + if (n->kind==FE_N_STRUCT) { + for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) { + sprintf(method_name,"%s_%s",n->text ? n->text : "Type", + m->text ? m->text : "method"); + m->cname=unit_cname(c,method_name); + } + } + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + t=n->a ? node_type(c,n->a) : unknown(c); + add_symbol(&s,s.globals,n->text,t,0,n->kind==FE_N_GLOBAL, + n->b!=0,unit_cname(c,n->text ? n->text : "global"),n); + } + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) { + t=fe_type_intern(&c->types,""); + add_symbol(&s,s.globals,n->text,t,n,0,1, + unit_cname(c,n->text ? n->text : "fn"),n); + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + sym=find_current(s.globals,n->text ? n->text : ""); + if (n->b) { + iv=m7_check_expected(&s,n->b,sym ? sym->type : 0); + if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { + sym->type=iv; + n->sem_type=iv; + } else if (sym && !fe_type_equal(sym->type,iv) && + !m7_actual_compatible(sym->type,iv,n->b)) + err(c,n->loc,"global initializer type mismatch"); + } + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) m7_check_fn(c,n,s.globals); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) { + t=fe_type_intern(&c->types,n->text); + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) m7_check_method(c,m,s.globals,t); + } + fe_type_layout_all(&c->types); + return c->diags->errors==0; +} + +FeType *fe_check_expr_type(FeCheck *c, FeNode *n) +{ + FeCheckerState s; + s.c=c; + s.scope=scope_new(&s,0); + s.globals=s.scope; + s.ret=fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=0; + fe_own_liveness_init(&s.liveness,&c->ast->arena); + if (m7_node_feature(n)) return m7_check_expr(&s,n); + return fe_check_expr_type_m6(c,n); +} From 8a2e6ddd1e36101fc4138f1327174b36dd6eb9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:52:57 +0900 Subject: [PATCH 075/184] implement M7 C emission --- fec/src/emit_c_m7.c | 1295 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1295 insertions(+) create mode 100644 fec/src/emit_c_m7.c diff --git a/fec/src/emit_c_m7.c b/fec/src/emit_c_m7.c new file mode 100644 index 0000000..ad971b1 --- /dev/null +++ b/fec/src/emit_c_m7.c @@ -0,0 +1,1295 @@ +/* M7 C backend integration. Keep the verified M1-M6 emitter available as + an exact fast path, while M7 sources reuse its mature helpers from this + translation unit and override only expression/control-flow semantics that + changed in v0.1.8. */ +#define type_needs_drop type_needs_drop_m6 +#define emit_expr emit_expr_m6 +#define emit_stmt emit_stmt_m6 +#define emit_block emit_block_m6 +#define emit_lvalue emit_lvalue_m6 +#define emit_decl emit_decl_m6 +#define emit_owned_live emit_owned_live_m6 +#define emit_value_drop emit_value_drop_m6 +#define emit_cleanup_block emit_cleanup_block_m6 +#define emit_cleanup_to emit_cleanup_to_m6 +#define emit_param_cleanup emit_param_cleanup_m6 +#define emit_cleanup_all emit_cleanup_all_m6 +#define emit_error_return emit_error_return_m6 +#define emit_match emit_match_m6 +#define emit_fn emit_fn_m6 +#define emit_main_wrapper emit_main_wrapper_m6 +#define emit_type_defs emit_type_defs_m6 +#define fe_emit_c_init fe_emit_c_init_m6 +#define fe_emit_c_program fe_emit_c_program_m6 +#include "emit_c.c" +#undef type_needs_drop +#undef emit_expr +#undef emit_stmt +#undef emit_block +#undef emit_lvalue +#undef emit_decl +#undef emit_owned_live +#undef emit_value_drop +#undef emit_cleanup_block +#undef emit_cleanup_to +#undef emit_param_cleanup +#undef emit_cleanup_all +#undef emit_error_return +#undef emit_match +#undef emit_fn +#undef emit_main_wrapper +#undef emit_type_defs +#undef fe_emit_c_init +#undef fe_emit_c_program + +#include "m7.h" +#include "lower.h" + +static void emit_expr(FeEmitter *e, FeNode *n); +static void emit_stmt(FeEmitter *e, FeNode *n); +static void emit_block(FeEmitter *e, FeNode *n); +static void emit_lvalue(FeEmitter *e, FeNode *n); + +static int type_needs_drop(FeType *t) +{ + return fe_lower_type_needs_drop(t); +} + +static const char *m7_c_type(FeEmitter *e, FeType *t) +{ + if (!t) return "long"; + if ((t->kind==FE_TYPE_ENUM && t->is_error) || + strcmp(t->name,"core.Error")==0) + return "unsigned short"; + return fe_type_c_name(t,e->pointer_bits); +} + +static int m7_node_feature(FeNode *n) +{ + FeNode *x; + if (!n) return 0; + if (n->sem_context) return 1; + if (n->sem_type && (n->sem_type->kind==FE_TYPE_OPTIONAL || + n->sem_type->kind==FE_TYPE_ERROR_UNION || + (n->sem_type->kind==FE_TYPE_ENUM && n->sem_type->is_error))) + return 1; + if (fe_m7_is_null(n) || fe_m7_is_try(n)) return 1; + if (n->kind==FE_N_MEMBER && n->text && strcmp(n->text,".?")==0) + return 1; + if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) + return 1; + if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) + return 1; + if (n->kind==FE_N_ARM && n->text && + (strcmp(n->text,"Some")==0 || strcmp(n->text,"None")==0)) + return 1; + if (m7_node_feature(n->a) || m7_node_feature(n->b) || + m7_node_feature(n->c)) return 1; + for (x=n->children;x;x=x->next) + if (m7_node_feature(x)) return 1; + return 0; +} + +static int m7_program_feature(FeEmitter *e) +{ + FeNode *n; + FeType *t; + if (!e || !e->check) return 0; + for (t=e->check->types.types;t;t=t->next) + if (t->kind==FE_TYPE_OPTIONAL || t->kind==FE_TYPE_ERROR_UNION || + (t->kind==FE_TYPE_ENUM && t->is_error)) return 1; + for (n=e->check->ast->root ? e->check->ast->root->children : 0; + n;n=n->next) + if (m7_node_feature(n) || n->kind==FE_N_ERROR_DECL) return 1; + return 0; +} + +static char *m7_temp_name(FeEmitter *e) +{ + char number[24]; + char *p; + unsigned long len; + sprintf(number,"%u",e->temp_serial++); + len=(unsigned long)strlen("fe_m7_tmp_")+ + (unsigned long)strlen(number)+1UL; + p=(char *)fe_arena_alloc(&e->check->ast->arena,len); + if (!p) return 0; + strcpy(p,"fe_m7_tmp_"); + strcat(p,number); + return p; +} + +static int m7_needs_temp(FeNode *n) +{ + if (!n) return 0; + if (fe_m7_is_try(n)) return 1; + if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) + return 1; + if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) + return 1; + if (n->kind==FE_N_MATCH && n->a && n->a->sem_type && + n->a->sem_type->kind==FE_TYPE_OPTIONAL) + return 1; + return 0; +} + +static FeType *m7_temp_type(FeNode *n) +{ + if (!n) return 0; + if (fe_m7_is_try(n)) return n->a ? n->a->sem_type : 0; + if (n->kind==FE_N_BINARY) return n->a ? n->a->sem_type : 0; + if ((n->kind==FE_N_IF || n->kind==FE_N_MATCH) && n->a) + return n->a->sem_type; + return 0; +} + +static void m7_prepare_temps(FeEmitter *e, FeNode *n) +{ + FeNode *x; + if (!n) return; + if (m7_needs_temp(n) && !n->aux_cname) + n->aux_cname=m7_temp_name(e); + m7_prepare_temps(e,n->a); + m7_prepare_temps(e,n->b); + m7_prepare_temps(e,n->c); + for (x=n->children;x;x=x->next) m7_prepare_temps(e,x); +} + +static void m7_emit_temp_decls(FeEmitter *e, FeNode *n) +{ + FeNode *x; + FeType *t; + if (!n) return; + if (m7_needs_temp(n) && n->aux_cname) { + t=m7_temp_type(n); + if (t) { + pad(e); fputs(m7_c_type(e,t),e->out); fputc(' ',e->out); + fputs(n->aux_cname,e->out); fputs(";\n",e->out); + } + } + m7_emit_temp_decls(e,n->a); + m7_emit_temp_decls(e,n->b); + m7_emit_temp_decls(e,n->c); + for (x=n->children;x;x=x->next) m7_emit_temp_decls(e,x); +} + +static void m7_emit_type(FeEmitter *e, FeType *t) +{ + unsigned i; + unsigned j; + if (!t || t->emit_state) return; + if (t->kind==FE_TYPE_OPTIONAL) { + t->emit_state=1; + m7_emit_type(e,t->elem); + if (!fe_m7_optional_uses_niche(t->elem) && t->cname) { + fputs(t->cname,e->out); fputs(" { unsigned char has; ",e->out); + fputs(m7_c_type(e,t->elem),e->out); + fputs(" v; };\n",e->out); + } + t->emit_state=2; + return; + } + if (t->kind==FE_TYPE_ARRAY) m7_emit_type(e,t->elem); + if (t->kind==FE_TYPE_SLICE) m7_emit_type(e,t->elem); + if (t->kind==FE_TYPE_OWNED) m7_emit_type(e,t->elem); + if (t->kind==FE_TYPE_STRUCT) + for (i=0;ifield_count;++i) m7_emit_type(e,t->fields[i].type); + if (t->kind==FE_TYPE_ENUM) + for (i=0;ivariant_count;++i) + for (j=0;jvariants[i].field_count;++j) + m7_emit_type(e,t->variants[i].fields[j].type); + if (t->kind==FE_TYPE_ERROR_UNION) { + m7_emit_type(e,t->elem); + m7_emit_type(e,t->error_value); + } + emit_one_type(e,t); +} + +static void emit_type_defs(FeEmitter *e) +{ + FeType *t; + for (t=e->check->types.types;t;t=t->next) + if (t->kind==FE_TYPE_ARRAY) + fe_type_slice(&e->check->types,t->elem); + for (t=e->check->types.types;t;t=t->next) m7_emit_type(e,t); +} + +static void m7_emit_drop_access(FeEmitter *e, FeType *t, + const char *access) +{ + if (!t || !access || !type_needs_drop(t)) return; + if (t->kind==FE_TYPE_OWNED) { + if (t->elem && t->elem->kind==FE_TYPE_SLICE) { + fprintf(e->out,"if ((%s).p) { free((%s).p); (%s).p=0; } ", + access,access,access); + } else { + fprintf(e->out,"if (%s) { ",access); + if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) + fprintf(e->out,"%s(%s); ",t->elem->drop_cname,access); + fprintf(e->out,"free(%s); %s=0; } ",access,access); + } + return; + } + if (t->drop_cname) + fprintf(e->out,"%s(&(%s)); ",t->drop_cname,access); +} + +static void m7_emit_drop_helpers(FeEmitter *e) +{ + FeType *t; + FeNode *method; + unsigned i; + unsigned j; + char access[256]; + for (t=e->check->types.types;t;t=t->next) + if (type_needs_drop(t) && t->drop_cname && + (t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY || + t->kind==FE_TYPE_OPTIONAL || t->kind==FE_TYPE_ERROR_UNION)) + fprintf(e->out,"static void %s(%s *self);\n", + t->drop_cname,m7_c_type(e,t)); + for (t=e->check->types.types;t;t=t->next) { + if (!type_needs_drop(t) || !t->drop_cname) continue; + if (t->kind==FE_TYPE_STRUCT) { + fprintf(e->out,"static void %s(%s *self) { ", + t->drop_cname,m7_c_type(e,t)); + method=find_drop_method(e,t->name); + if (method) fprintf(e->out,"%s(self); ",cname(method,"fe_drop_method")); + for (i=t->field_count;i>0;--i) { + sprintf(access,"self->%s",t->fields[i-1U].name); + m7_emit_drop_access(e,t->fields[i-1U].type,access); + } + fputs("}\n",e->out); + } else if (t->kind==FE_TYPE_ARRAY) { + fprintf(e->out,"static void %s(%s *self) { unsigned long i; for (i=0; i<%lu; ++i) { ", + t->drop_cname,m7_c_type(e,t),t->length); + strcpy(access,"self->a[i]"); + m7_emit_drop_access(e,t->elem,access); + fputs("} }\n",e->out); + } else if (t->kind==FE_TYPE_OPTIONAL) { + fprintf(e->out,"static void %s(%s *self) { ", + t->drop_cname,m7_c_type(e,t)); + if (fe_m7_optional_uses_niche(t->elem)) { + fputs("if (*self) { ",e->out); + m7_emit_drop_access(e,t->elem,"*self"); + fputs("} ",e->out); + } else { + fputs("if (self->has) { ",e->out); + m7_emit_drop_access(e,t->elem,"self->v"); + fputs("self->has=0; } ",e->out); + } + fputs("}\n",e->out); + } else if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && + t->error_value->kind!=FE_TYPE_VOID) { + fprintf(e->out,"static void %s(%s *self) { if (!self->e) { ", + t->drop_cname,m7_c_type(e,t)); + m7_emit_drop_access(e,t->error_value,"self->v"); + fputs("self->e=1; } }\n",e->out); + } + } + /* Error enums are scalar codes, so their enum payload helper functions + from M3 are deliberately not emitted in the M7 path. */ + (void)j; +} + +static void m7_emit_type_helpers(FeEmitter *e) +{ + FeType *t; + unsigned i; + unsigned j; + const char *ct; + FeVariantType *v; + for (t=e->check->types.types;t;t=t->next) { + if (t->kind==FE_TYPE_OPTIONAL) { + if (!fe_m7_optional_uses_niche(t->elem)) { + fprintf(e->out,"static %s %s(%s v) { %s r; r.has=1; r.v=v; return r; }\n", + m7_c_type(e,t),t->maker,m7_c_type(e,t->elem),m7_c_type(e,t)); + fprintf(e->out,"static %s %s(void) { %s r; memset(&r,0,sizeof(r)); return r; }\n", + m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); + fprintf(e->out,"static %s %s(%s x) { ", + m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); + if (!e->no_checks) fputs("if (!x.has) fe_trap_bounds(); ",e->out); + fputs("return x.v; }\n",e->out); + } else { + fprintf(e->out,"static %s %s(%s x) { ", + m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); + if (!e->no_checks) fputs("if (!x) fe_trap_bounds(); ",e->out); + fputs("return x; }\n",e->out); + } + } + if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && + t->error_value->kind!=FE_TYPE_VOID) { + fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", + m7_c_type(e,t),t->maker,m7_c_type(e,t->error_value),m7_c_type(e,t)); + if (t->none_cname) + fprintf(e->out,"static %s %s(unsigned short e) { %s r; memset(&r,0,sizeof(r)); r.e=e; return r; }\n", + m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); + if (t->error_value->kind==FE_TYPE_OWNED && + t->error_value->elem && t->error_value->elem->kind==FE_TYPE_SLICE) { + FeType *item=t->error_value->elem->elem; + fprintf(e->out,"static %s %s(unsigned long n) { %s r; r.v.p=(%s*)malloc(sizeof(%s)*n); r.v.n=n; r.e=(r.v.p || !n) ? 0 : 1; return r; }\n", + m7_c_type(e,t),t->alloc_cname,m7_c_type(e,t), + m7_c_type(e,item),m7_c_type(e,item)); + } else if (t->error_value->kind==FE_TYPE_OWNED) { + fprintf(e->out,"static %s %s(%s v) { %s r; r.v=(%s)malloc(sizeof(%s)); if(r.v) *r.v=v; r.e=r.v ? 0 : 1; return r; }\n", + m7_c_type(e,t),t->alloc_cname, + m7_c_type(e,t->error_value->elem),m7_c_type(e,t), + m7_c_type(e,t->error_value), + m7_c_type(e,t->error_value->elem)); + } + } + } + for (t=e->check->types.types;t;t=t->next) { + if (t->replace_cname) { + ct=m7_c_type(e,t); + fprintf(e->out,"static %s %s(%s *dst, %s value) { %s old=*dst; *dst=value; return old; }\n", + ct,t->replace_cname,ct,ct,ct); + } + if (t->kind==FE_TYPE_STRUCT && t->maker) { + fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); + for (i=0;ifield_count;i++) { + if (i) fputs(", ",e->out); + fputs(m7_c_type(e,t->fields[i].type),e->out); + fprintf(e->out," p%u",i); + } + fprintf(e->out,") { %s v;",m7_c_type(e,t)); + for (i=0;ifield_count;i++) + fprintf(e->out," v.%s=p%u;",t->fields[i].name,i); + fputs(" return v; }\n",e->out); + } else if (t->kind==FE_TYPE_ARRAY && t->maker) { + fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); + for (i=0;ilength;i++) { + if (i) fputs(", ",e->out); + fputs(m7_c_type(e,t->elem),e->out); + fprintf(e->out," p%u",i); + } + fprintf(e->out,") { %s v;",m7_c_type(e,t)); + for (i=0;ilength;i++) fprintf(e->out," v.a[%u]=p%u;",i,i); + fputs(" return v; }\n",e->out); + } else if (t->kind==FE_TYPE_ENUM && !t->is_error) { + for (i=0;ivariant_count;i++) { + v=&t->variants[i]; + fprintf(e->out,"static %s %s(",m7_c_type(e,t),v->maker); + for (j=0;jfield_count;j++) { + if (j) fputs(", ",e->out); + fputs(m7_c_type(e,v->fields[j].type),e->out); + fprintf(e->out," p%u",j); + } + fprintf(e->out,") { %s x; x.tag=%u;",m7_c_type(e,t),v->tag); + for (j=0;jfield_count;j++) { + if (v->field_count==1) + fprintf(e->out," x.payload.%s=p%u;",v->name,j); + else + fprintf(e->out," x.payload.%s.%s=p%u;",v->name, + v->fields[j].name,j); + } + fputs(" return x; }\n",e->out); + } + } + } + m7_emit_drop_helpers(e); + /* Reuse the mature M3 index/slice helper generator. It does not depend + on M7 drop policy and all wrapper dependencies are already emitted. */ + for (t=e->check->types.types;t;t=t->next) { + if (t->kind==FE_TYPE_ARRAY && t->indexer) { + fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", + m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); + if (!e->no_checks) + fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); + fputs("return x.a[i]; }\n",e->out); + } else if (t->kind==FE_TYPE_SLICE && t->indexer) { + fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", + m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); + if (!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); + fputs("return x.p[i]; }\n",e->out); + } + } +} + +static void m7_emit_present(FeEmitter *e, FeType *opt, const char *name) +{ + if (fe_m7_optional_uses_niche(opt->elem)) { + fputs("(",e->out); fputs(name,e->out); fputs(" != 0)",e->out); + } else { + fputs(name,e->out); fputs(".has",e->out); + } +} + +static void m7_emit_payload_var(FeEmitter *e, FeType *opt, const char *name) +{ + (void)e; + fputs(name,e->out); + if (!fe_m7_optional_uses_niche(opt->elem)) fputs(".v",e->out); +} + +static void m7_emit_error_member(FeEmitter *e, FeNode *n) +{ + FeVariantType *v; + FeType *t; + t=n && n->a ? n->a->sem_type : 0; + v=t && t->kind==FE_TYPE_ENUM ? + fe_type_variant(t,n->b ? n->b->text : "") : 0; + if (v) fprintf(e->out,"%u",v->tag); + else fputs("0",e->out); +} + +static void m7_emit_raw_expr(FeEmitter *e, FeNode *n); + +static void m7_emit_contextual(FeEmitter *e, FeNode *n) +{ + FeType *ctx; + FeType *actual; + FeType *error_type; + ctx=n ? n->sem_context : 0; + actual=n ? n->sem_type : 0; + if (!ctx) { m7_emit_raw_expr(e,n); return; } + if (ctx->kind==FE_TYPE_OPTIONAL) { + if (fe_m7_is_null(n)) { + if (fe_m7_optional_uses_niche(ctx->elem)) fputs("0",e->out); + else { fputs(ctx->none_cname,e->out); fputs("()",e->out); } + return; + } + if (fe_m7_optional_uses_niche(ctx->elem)) { + m7_emit_raw_expr(e,n); + } else { + fputs(ctx->maker,e->out); fputc('(',e->out); + m7_emit_raw_expr(e,n); fputc(')',e->out); + } + return; + } + if (ctx->kind==FE_TYPE_ERROR_UNION) { + error_type=ctx->elem; + if (!error_type) error_type=fe_type_intern(&e->check->types,"core.Error"); + if (actual && fe_type_equal(actual,ctx->error_value)) { + if (ctx->error_value->kind==FE_TYPE_VOID) fputs("0",e->out); + else { + fputs(ctx->maker,e->out); fputs("(0, ",e->out); + m7_emit_raw_expr(e,n); fputc(')',e->out); + } + return; + } + if (actual && fe_type_equal(actual,error_type)) { + if (ctx->error_value->kind==FE_TYPE_VOID) + m7_emit_raw_expr(e,n); + else { + fputs(ctx->none_cname,e->out); fputc('(',e->out); + m7_emit_raw_expr(e,n); fputc(')',e->out); + } + return; + } + } + m7_emit_raw_expr(e,n); +} + +static void emit_expr(FeEmitter *e, FeNode *n) +{ + if (!n) { fputs("0",e->out); return; } + if (n->sem_context) m7_emit_contextual(e,n); + else m7_emit_raw_expr(e,n); +} + +static void emit_lvalue(FeEmitter *e, FeNode *n) +{ + FeType *bt; + if (!n) { fputs("fe_bad_lvalue",e->out); return; } + if (n->kind==FE_N_IDENT) { + fputs(cname(n,"fe_local"),e->out); + return; + } + if (n->kind==FE_N_MEMBER) { + bt=n->a ? n->a->sem_type : 0; + if (n->text && strcmp(n->text,".?")==0) { + emit_expr(e,n); + return; + } + if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); + } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { + emit_expr(e,n->a); fputs("->",e->out); + fputs(n->b && n->b->text ? n->b->text : "member",e->out); + } else { + emit_lvalue(e,n->a); fputc('.',e->out); + fputs(n->b && n->b->text ? n->b->text : "member",e->out); + } + return; + } + if (n->kind==FE_N_INDEX) { + bt=n->a ? n->a->sem_type : 0; + emit_lvalue(e,n->a); + fputs(bt && bt->kind==FE_TYPE_ARRAY ? ".a[" : ".p[",e->out); + emit_expr(e,n->b); fputc(']',e->out); + return; + } + m7_emit_raw_expr(e,n); +} + +static void m7_emit_call(FeEmitter *e, FeNode *n) +{ + FeNode *x; + FeNode *call_param; + FeVariantType *v; + int special; + call_param=0; + special=0; + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"destroy")==0 && n->children) { + FeNode *arg=n->children; + fputs("(free(",e->out); emit_expr(e,arg); + if (arg->sem_type && arg->sem_type->kind==FE_TYPE_OWNED && + arg->sem_type->elem && arg->sem_type->elem->kind==FE_TYPE_SLICE) + fputs(".p",e->out); + fputc(')',e->out); + if (arg->kind==FE_N_IDENT) { + fputs(", ",e->out); emit_lvalue(e,arg); + if (arg->sem_type && arg->sem_type->elem && + arg->sem_type->elem->kind==FE_TYPE_SLICE) fputs(".p=0",e->out); + else fputs("=0",e->out); + fputs(", fe_live_",e->out); fputs(cname(arg,"owned"),e->out); + fputs("=0",e->out); + } + fputs(", 0)",e->out); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"replace")==0 && n->children && + n->children->next && n->sem_type) { + fputs(n->sem_type->replace_cname ? n->sem_type->replace_cname : + "fe_bad_replace",e->out); + fputc('(',e->out); emit_expr(e,n->children); fputs(", ",e->out); + emit_expr(e,n->children->next); fputc(')',e->out); + return; + } + if (n->text && (strcmp(n->text,"@print")==0 || + strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { + emit_m4_builtin(e,n); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"null_writer")==0) { + fputs("fe_m4_null_writer()",e->out); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM && + !n->a->a->sem_type->is_error) { + v=fe_type_variant(n->a->a->sem_type,n->a->b ? n->a->b->text : ""); + fputs(v ? v->maker : "fe_bad_variant",e->out); + } else if (n->a && n->a->kind==FE_N_MEMBER && n->sem_decl && + n->sem_decl->kind==FE_N_FN) { + FeNode *mp=n->sem_decl->a ? n->sem_decl->a->children : 0; + FeNode *ma; + fputs(cname(n->sem_decl,"fe_method"),e->out); fputc('(',e->out); + if (mp && mp->sem_type && mp->sem_type->kind==FE_TYPE_REF) { + fputc('&',e->out); emit_lvalue(e,n->a->a); + } else emit_expr(e,n->a->a); + for (ma=n->children;ma;ma=ma->next) { + fputs(", ",e->out); emit_expr(e,ma); + } + fputc(')',e->out); + return; + } else if (n->a) emit_expr(e,n->a); + else fputs(n->text ? n->text : "fe_builtin",e->out); + if (!special) { + if (n->sem_decl && n->sem_decl->kind==FE_N_FN && n->sem_decl->a) + call_param=n->sem_decl->a->children; + fputc('(',e->out); + for (x=n->children;x;x=x->next) { + FeType *want=call_param && call_param->a ? + fe_type_from_ast(&e->check->types,call_param->a) : 0; + if (x!=n->children) fputs(", ",e->out); + if (want && want->kind==FE_TYPE_SLICE && !want->ref_mut && + x->sem_type && x->sem_type->kind==FE_TYPE_SLICE && + x->sem_type->ref_mut) { + fputs(want->maker,e->out); fputc('(',e->out); + emit_expr(e,x); fputs(".p, ",e->out); + emit_expr(e,x); fputs(".n)",e->out); + } else emit_expr(e,x); + if (call_param) call_param=call_param->next; + } + fputc(')',e->out); + } +} + +static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) +{ + FeNode *x; + FeType *bt; + FeVariantType *v; + const char *op; + FeM7LazyKind lazy; + if (!n) { fputs("0",e->out); return; } + if (!m7_node_feature(n)) { + emit_expr_m6(e,n); + return; + } + switch (n->kind) { + case FE_N_IDENT: + if ((n->flags & FE_OWN_NODE_CONSUMED) && n->sem_type && + type_needs_drop(n->sem_type)) { + fputs("(fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("=0, ",e->out); fputs(cname(n,"fe_missing"),e->out); + fputc(')',e->out); + } else fputs(cname(n,"fe_missing"),e->out); + break; + case FE_N_LITERAL: + if (fe_m7_is_null(n)) fputs("0",e->out); + else emit_expr_m6(e,n); + break; + case FE_N_UNARY: + op=n->text ? n->text : ""; + if (strcmp(op,"try")==0) { + if (n->a && n->a->sem_type && + n->a->sem_type->kind==FE_TYPE_ERROR_UNION && + n->a->sem_type->error_value && + n->a->sem_type->error_value->kind!=FE_TYPE_VOID) { + fputs("(",e->out); fputs(n->aux_cname,e->out); + fputs(" = ",e->out); emit_expr(e,n->a); fputs(", ",e->out); + fputs(n->aux_cname,e->out); fputs(".v)",e->out); + } else emit_expr(e,n->a); + } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + fputs("(&",e->out); emit_lvalue(e,n->a); fputc(')',e->out); + } else if (strcmp(op,"not")==0) { + fputs("(!",e->out); emit_expr(e,n->a); fputc(')',e->out); + } else { + fputc('(',e->out); fputs(op,e->out); emit_expr(e,n->a); + fputc(')',e->out); + } + break; + case FE_N_BINARY: + lazy=fe_m7_lazy_kind(n); + if (lazy==FE_M7_LAZY_ORELSE) { + FeType *opt=n->a ? n->a->sem_type : 0; + fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs("), ",e->out); + m7_emit_present(e,opt,n->aux_cname); fputs(" ? ",e->out); + if (fe_m7_optional_uses_niche(opt->elem)) fputs(n->aux_cname,e->out); + else { fputs(n->aux_cname,e->out); fputs(".v",e->out); } + fputs(" : ",e->out); emit_expr(e,n->b); fputc(')',e->out); + } else if (lazy==FE_M7_LAZY_CATCH && !n->c) { + FeType *res=n->a ? n->a->sem_type : 0; + fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs("), ",e->out); + if (res && res->error_value && res->error_value->kind==FE_TYPE_VOID) { + fputs(n->aux_cname,e->out); fputs(" ? ",e->out); + emit_expr(e,n->b); fputs(" : 0)",e->out); + } else { + fputs(n->aux_cname,e->out); fputs(".e ? ",e->out); + emit_expr(e,n->b); fputs(" : ",e->out); + fputs(n->aux_cname,e->out); fputs(".v)",e->out); + } + } else if (lazy==FE_M7_LAZY_CATCH && n->c) { + fputs("0",e->out); + } else if ((n->text && (strcmp(n->text,"==")==0 || + strcmp(n->text,"!=")==0)) && + (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { + FeNode *value=fe_m7_is_null(n->a) ? n->b : n->a; + FeType *opt=value ? value->sem_type : 0; + if (opt && opt->kind==FE_TYPE_OPTIONAL && + !fe_m7_optional_uses_niche(opt->elem)) { + fputs("(!",e->out); emit_expr(e,value); fputs(".has)",e->out); + if (strcmp(n->text,"!=")==0) { + fputs(" == 0",e->out); + } + } else { + fputc('(',e->out); emit_expr(e,value); + fputs(strcmp(n->text,"==")==0 ? " == 0)" : " != 0)",e->out); + } + } else { + op=n->text ? n->text : "+"; + fputc('(',e->out); emit_expr(e,n->a); + if (strcmp(op,"and")==0) fputs(" && ",e->out); + else if (strcmp(op,"or")==0) fputs(" || ",e->out); + else fputs(op,e->out); + emit_expr(e,n->b); fputc(')',e->out); + } + break; + case FE_N_MEMBER: + bt=n->a ? n->a->sem_type : 0; + if (n->text && strcmp(n->text,".?")==0 && bt && + bt->kind==FE_TYPE_OPTIONAL) { + fputs(bt->unwrap_cname,e->out); fputc('(',e->out); + emit_expr(e,n->a); fputc(')',e->out); + } else if (bt && bt->kind==FE_TYPE_ENUM && bt->is_error) { + m7_emit_error_member(e,n); + } else if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); + } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { + emit_expr(e,n->a); fputs("->",e->out); + fputs(n->b && n->b->text ? n->b->text : "member",e->out); + } else if (bt && bt->kind==FE_TYPE_ENUM && !bt->is_error) { + v=fe_type_variant(bt,n->b ? n->b->text : ""); + if (v) { fputs(v->maker,e->out); fputs("()",e->out); } + else fputs("0",e->out); + } else { + emit_expr(e,n->a); fputc('.',e->out); + if (n->b) fputs(n->b->text ? n->b->text : "member",e->out); + } + break; + case FE_N_CALL: + m7_emit_call(e,n); + break; + case FE_N_TYPE: + if (n->text && strcmp(n->text,"as")==0) { + fputs("((",e->out); fputs(m7_c_type(e,n->sem_type),e->out); + fputc(')',e->out); emit_expr(e,n->a); fputc(')',e->out); + } else emit_expr(e,n->a); + break; + case FE_N_INDEX: + bt=n->a ? n->a->sem_type : 0; + if (n->c || !n->b) { + emit_expr_m6(e,n); + } else if (bt && bt->indexer) { + fputs(bt->indexer,e->out); fputc('(',e->out); + emit_expr(e,n->a); fputs(", ",e->out); emit_expr(e,n->b); + fputc(')',e->out); + } else fputs("0",e->out); + break; + case FE_N_STRUCT_INIT: + case FE_N_ARRAY_INIT: + emit_expr_m6(e,n); + break; + default: + emit_expr_m6(e,n); + break; + } + (void)x; +} + +static void emit_decl(FeEmitter *e, FeNode *n) +{ + pad(e); fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); + fputs(cname(n,"fe_local"),e->out); + if (n->kind==FE_N_CONST && n->b) { + fputs(" = ",e->out); emit_expr(e,n->b); + } + fputs(";\n",e->out); + if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && + type_needs_drop(n->sem_type)) { + pad(e); fputs("unsigned char fe_live_",e->out); + fputs(cname(n,"owned"),e->out); fputs("=0;\n",e->out); + } +} + +static void emit_owned_live(FeEmitter *e, FeNode *n, int value) +{ + if (n && n->sem_type && type_needs_drop(n->sem_type)) { + pad(e); fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fprintf(e->out,"=%d;\n",value); + } +} + +static void emit_value_drop(FeEmitter *e, FeNode *n) +{ + FeType *t; + t=n ? n->sem_type : 0; + if (!n || !t || !type_needs_drop(t) || + (n->flags & FE_OWN_NODE_CONSUMED) || + (n->flags & FE_OWN_NODE_DEFER_CAPTURE)) return; + pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs(") { ",e->out); + if (t->kind==FE_TYPE_OWNED) { + if (t->elem && t->elem->kind==FE_TYPE_SLICE) { + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); + fputs(".p); ",e->out); + } else { + if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) + fprintf(e->out,"%s(%s); ",t->elem->drop_cname,cname(n,"owned")); + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); + fputs("); ",e->out); + } + } else if (t->drop_cname) { + fprintf(e->out,"%s(&%s); ",t->drop_cname,cname(n,"local")); + } + fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("=0; }\n",e->out); +} + +static void emit_cleanup_block(FeEmitter *e, FeNode *n) +{ + FeNode *x; + unsigned count; + unsigned index; + unsigned seen; + unsigned depth; + count=0; + seen=0xffffffffU; + for (depth=0;depthblock_depth;++depth) + if (e->block_stack[depth]==n) { + seen=e->block_seen[depth]; + break; + } + for (x=n ? n->children : 0,index=0;x;x=x->next,++index) + if (indexkind==FE_N_DEFER || x->kind==FE_N_LET || + x->kind==FE_N_VAR)) ++count; + while (count) { + index=0; + for (x=n->children;x;x=x->next) + if ((x->kind==FE_N_DEFER || x->kind==FE_N_LET || + x->kind==FE_N_VAR) && index++==count-1U) { + if (x->kind==FE_N_DEFER) emit_stmt(e,x->a); + else emit_value_drop(e,x); + break; + } + --count; + } +} + +static void emit_cleanup_to(FeEmitter *e, unsigned floor) +{ + unsigned i; + for (i=e->block_depth;i>floor;--i) + emit_cleanup_block(e,e->block_stack[i-1U]); +} + +static void emit_param_cleanup(FeEmitter *e) +{ + FeNode *p; + if (!e->current_fn || !e->current_fn->a) return; + for (p=e->current_fn->a->children;p;p=p->next) emit_value_drop(e,p); +} + +static void emit_cleanup_all(FeEmitter *e) +{ + emit_cleanup_to(e,0); + emit_param_cleanup(e); +} + +static void emit_error_return(FeEmitter *e, const char *error_expr) +{ + FeType *ret=e->current_ret; + if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && + ret->error_value->kind!=FE_TYPE_VOID) { + fputs("return ",e->out); fputs(ret->none_cname,e->out); + fputc('(',e->out); fputs(error_expr,e->out); fputs(");\n",e->out); + } else { + fputs("return ",e->out); fputs(error_expr,e->out); fputs(";\n",e->out); + } +} + +static void m7_emit_try_error_check(FeEmitter *e, FeNode *n) +{ + FeType *result=n->a ? n->a->sem_type : 0; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + emit_cleanup_all(e); + pad(e); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) { + char error[192]; + sprintf(error,"%s.e",n->aux_cname); + emit_error_return(e,error); + } else emit_error_return(e,n->aux_cname); + --e->indent; pad(e); fputs("}\n",e->out); +} + +static void m7_emit_catch_block(FeEmitter *e, FeNode *n, + FeNode *target) +{ + FeType *result=n->a ? n->a->sem_type : 0; + FeNode *binding=n->b; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + if (binding && binding->cname) { + pad(e); fputs("unsigned short ",e->out); fputs(binding->cname,e->out); + fputs(" = ",e->out); fputs(n->aux_cname,e->out); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(";\n",e->out); + } + emit_stmt(e,n->c); + --e->indent; pad(e); fputs("}",e->out); + if (target && result && result->error_value && + result->error_value->kind!=FE_TYPE_VOID) { + fputs(" else {\n",e->out); ++e->indent; + pad(e); emit_lvalue(e,target); fputs(" = ",e->out); + fputs(n->aux_cname,e->out); fputs(".v;\n",e->out); + emit_owned_live(e,target,1); + --e->indent; pad(e); fputs("}",e->out); + } + fputc('\n',e->out); +} + +static void m7_emit_optional_match(FeEmitter *e, FeNode *n) +{ + FeType *opt=n->a ? n->a->sem_type : 0; + FeNode *arm; + FeNode *binding; + int first; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + first=1; + for (arm=n->children;arm;arm=arm->next) { + if (arm->text && strcmp(arm->text,"Some")==0) { + pad(e); if (!first) fputs("else ",e->out); + fputs("if (",e->out); m7_emit_present(e,opt,n->aux_cname); + fputs(") {\n",e->out); ++e->indent; + binding=arm->children; + if (binding && binding->cname) { + pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); + fputc(' ',e->out); fputs(binding->cname,e->out); fputs(" = ",e->out); + m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); + } + if (arm->a) emit_stmt(e,arm->a); + --e->indent; pad(e); fputs("}\n",e->out); + first=0; + } else if (arm->text && strcmp(arm->text,"None")==0) { + pad(e); if (!first) fputs("else ",e->out); + fputs("if (!",e->out); m7_emit_present(e,opt,n->aux_cname); + fputs(") {\n",e->out); ++e->indent; + if (arm->a) emit_stmt(e,arm->a); + --e->indent; pad(e); fputs("}\n",e->out); + first=0; + } else if (arm->text && strcmp(arm->text,"_")==0) { + pad(e); if (!first) fputs("else ",e->out); + fputs("{\n",e->out); ++e->indent; + if (arm->a) emit_stmt(e,arm->a); + --e->indent; pad(e); fputs("}\n",e->out); + first=0; + } + } +} + +static void m7_emit_if_let(FeEmitter *e, FeNode *n) +{ + FeType *opt=n->a ? n->a->sem_type : 0; + FeNode *binding=n->children; + int some=n->aux_text && strcmp(n->aux_text,"Some")==0; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); + if (!some) fputc('!',e->out); + m7_emit_present(e,opt,n->aux_cname); fputs(") {\n",e->out); + ++e->indent; + if (some && binding && binding->cname) { + pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); fputc(' ',e->out); + fputs(binding->cname,e->out); fputs(" = ",e->out); + m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); + } + if (n->b) emit_stmt(e,n->b); + --e->indent; pad(e); fputs("}",e->out); + if (n->c) { + fputs(" else ",e->out); + emit_stmt(e,n->c); + } + fputc('\n',e->out); +} + +static void emit_block(FeEmitter *e, FeNode *n) +{ + FeNode *x; + unsigned seen; + if (!n) { + pad(e); fputs("{}",e->out); return; + } + pad(e); fputs("{\n",e->out); ++e->indent; + if (e->block_depth<32U) { + e->block_stack[e->block_depth]=n; + e->block_seen[e->block_depth]=0; + ++e->block_depth; + } + for (x=n->children;x;x=x->next) + if (x->kind==FE_N_LET || x->kind==FE_N_VAR || x->kind==FE_N_CONST) + emit_decl(e,x); + if (e->current_fn && e->current_fn->c==n) { + if (e->current_fn->a) { + FeNode *p; + for (p=e->current_fn->a->children;p;p=p->next) + if (p->sem_type && type_needs_drop(p->sem_type)) { + pad(e); fputs("unsigned char fe_live_",e->out); + fputs(cname(p,"owned"),e->out); fputs("=1;\n",e->out); + } + } + m7_emit_temp_decls(e,n); + } + if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs(m7_c_type(e,e->current_ret),e->out); + fputs(" fe_return_value;\n",e->out); + } + seen=0; + for (x=n->children;x;x=x->next) { + ++seen; + if (e->block_depth) e->block_seen[e->block_depth-1U]=seen; + emit_stmt(e,x); + } + --e->indent; + emit_cleanup_block(e,n); + if (e->current_fn && e->current_fn->c==n) emit_param_cleanup(e); + if (e->block_depth) --e->block_depth; + if (e->fallthrough_block==n) { + pad(e); fputs("return 0;\n",e->out); + e->fallthrough_block=0; + } + pad(e); fputc('}',e->out); +} + +static void emit_stmt(FeEmitter *e, FeNode *n) +{ + FeType *result; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + emit_block(e,n); fputc('\n',e->out); break; + case FE_N_LET: + case FE_N_VAR: + if (n->b) { + if (fe_m7_is_try(n->b)) { + m7_emit_try_error_check(e,n->b); + pad(e); emit_lvalue(e,n); fputs(" = ",e->out); + fputs(n->b->aux_cname,e->out); + result=n->b->a ? n->b->a->sem_type : 0; + if (result && result->error_value && + result->error_value->kind!=FE_TYPE_VOID) fputs(".v",e->out); + fputs(";\n",e->out); emit_owned_live(e,n,1); + } else if (n->b->kind==FE_N_BINARY && n->b->c && + fe_m7_lazy_kind(n->b)==FE_M7_LAZY_CATCH) { + m7_emit_catch_block(e,n->b,n); + } else { + pad(e); emit_lvalue(e,n); fputs(" = ",e->out); + emit_expr(e,n->b); fputs(";\n",e->out); + emit_owned_live(e,n,1); + } + } + break; + case FE_N_ASSIGN: + if (n->a && n->a->kind==FE_N_IDENT) emit_value_drop(e,n->a); + pad(e); emit_lvalue(e,n->a); fputc(' ',e->out); + fputs(n->text ? n->text : "=",e->out); fputc(' ',e->out); + emit_expr(e,n->b); fputs(";\n",e->out); + if (n->a && n->a->kind==FE_N_IDENT) emit_owned_live(e,n->a,1); + break; + case FE_N_EXPR_STMT: + if (fe_m7_is_try(n->a)) { + m7_emit_try_error_check(e,n->a); + } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && + fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { + m7_emit_catch_block(e,n->a,0); + } else { + pad(e); emit_expr(e,n->a); fputs(";\n",e->out); + } + break; + case FE_N_DEFER: + break; + case FE_N_RETURN: + if (n->a && fe_m7_is_try(n->a)) { + FeNode *tr=n->a; + FeType *res=tr->a ? tr->a->sem_type : 0; + m7_emit_try_error_check(e,tr); + if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs("fe_return_value = ",e->out); + if (e->current_ret->kind==FE_TYPE_ERROR_UNION && + e->current_ret->error_value && + e->current_ret->error_value->kind!=FE_TYPE_VOID) { + fputs(e->current_ret->maker,e->out); fputs("(0, ",e->out); + fputs(tr->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".v",e->out); + fputc(')',e->out); + } else { + fputs(tr->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".v",e->out); + } + fputs(";\n",e->out); + } + emit_cleanup_all(e); + pad(e); fputs("return fe_return_value;\n",e->out); + } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && + fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { + /* A value catch-block is lowered as a temporary local success + assignment; the handler is required by the checker to exit. */ + FeNode *cx=n->a; + FeType *res=cx->a ? cx->a->sem_type : 0; + pad(e); fputs(cx->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,cx->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + if (cx->b && cx->b->cname) { + pad(e); fputs("unsigned short ",e->out); fputs(cx->b->cname,e->out); + fputs(" = ",e->out); fputs(cx->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(";\n",e->out); + } + emit_stmt(e,cx->c); + --e->indent; pad(e); fputs("}\n",e->out); + pad(e); fputs("fe_return_value = ",e->out); + fputs(cx->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".v",e->out); + fputs(";\n",e->out); + emit_cleanup_all(e); + pad(e); fputs("return fe_return_value;\n",e->out); + } else { + if (n->a && e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs("fe_return_value = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + } + emit_cleanup_all(e); + pad(e); fputs("return",e->out); + if (n->a) fputs(" fe_return_value",e->out); + fputs(";\n",e->out); + } + break; + case FE_N_IF: + if (n->text && strcmp(n->text,"if let")==0) { + m7_emit_if_let(e,n); + } else { + pad(e); fputs("if (",e->out); emit_expr(e,n->a); fputs(") ",e->out); + emit_block(e,n->b); + if (n->c) { + fputs(" else ",e->out); + if (n->c->kind==FE_N_IF) emit_stmt(e,n->c); + else emit_block(e,n->c); + } + fputc('\n',e->out); + } + break; + case FE_N_MATCH: + if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OPTIONAL) + m7_emit_optional_match(e,n); + else emit_match_m6(e,n,0); + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (e->loop_depth) { + emit_cleanup_to(e,e->loop_floor[e->loop_depth-1U]); + pad(e); fputs(n->kind==FE_N_BREAK ? "break;\n" : "continue;\n",e->out); + } + break; + case FE_N_WHILE: + pad(e); fputs("while (",e->out); emit_expr(e,n->a); fputs(") ",e->out); + if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; + emit_block(e,n->b); + if (e->loop_depth) --e->loop_depth; + fputc('\n',e->out); + break; + case FE_N_FOR: + emit_stmt_m6(e,n); + break; + default: + emit_stmt_m6(e,n); + break; + } +} + +static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) +{ + FeNode *p; + FeType *old_ret; + FeNode *old_fn; + fputs(m7_c_type(e,fn->sem_type ? fn->sem_type : + (fn->b ? fe_type_from_ast(&e->check->types,fn->b) : + fe_type_intern(&e->check->types,"void"))),e->out); + fputc(' ',e->out); fputs(cname(fn,"fe_fn"),e->out); fputc('(',e->out); + p=fn->a ? fn->a->children : 0; + if (!p) fputs("void",e->out); + while (p) { + if (p!=fn->a->children) fputs(", ",e->out); + fputs(m7_c_type(e,p->sem_type ? p->sem_type : + fe_type_from_ast(&e->check->types,p->a)),e->out); + fputc(' ',e->out); fputs(cname(p,"fe_arg"),e->out); + p=p->next; + } + fputc(')',e->out); + if (prototype) { fputs(";\n",e->out); return; } + old_ret=e->current_ret; + old_fn=e->current_fn; + e->current_ret=fn->sem_type; + e->current_fn=fn; + m7_prepare_temps(e,fn->c); + fputc(' ',e->out); + if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && + fn->sem_type->error_value && fn->sem_type->error_value->kind==FE_TYPE_VOID) + e->fallthrough_block=fn->c; + emit_block(e,fn->c); + e->current_ret=old_ret; + e->current_fn=old_fn; + fputc('\n',e->out); +} + +static void emit_main_wrapper(FeEmitter *e, FeNode *fn) +{ + FeType *ret=fn->sem_type; + if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && + ret->error_value->kind!=FE_TYPE_VOID) { + fputs("int main(void) { ",e->out); fputs(m7_c_type(e,ret),e->out); + fputs(" r = ",e->out); fputs(cname(fn,"fe_main"),e->out); + fputs("(); return r.e ? 1 : 0; }\n",e->out); + } else emit_main_wrapper_m6(e,fn); +} + +void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, + unsigned pointer_bits, int no_checks) +{ + fe_emit_c_init_m6(e,out,check,pointer_bits,no_checks); +} + +void fe_emit_c_program(FeEmitter *e) +{ + FeNode *n; + FeNode *main_fn; + FeType *type; + int need_m4; + if (!m7_program_feature(e)) { + fe_emit_c_program_m6(e); + return; + } + main_fn=0; + need_m4=node_uses_m4(e->check->ast->root); + for (type=e->check->types.types;type;type=type->next) + if (strcmp(type->name,"io.Writer")==0) need_m4=1; + fputs("/* generated by fec M7 */\n#include \n#include \n#include \n#include \ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); + if (e->pointer_bits==16) + fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); + else + fputs("typedef char fe_assert_usize[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); + fputs("static void fe_trap_bounds(void) { abort(); }\nstatic unsigned short fe_error_temp;\n\n",e->out); + emit_type_defs(e); + if (need_m4) emit_m4_runtime(e); + m7_emit_type_helpers(e); + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) { + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); + fputs(cname(n,"fe_global"),e->out); + if (n->b) { fputs(" = ",e->out); emit_expr(e,n->b); } + fputs(";\n",e->out); + } + } + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) { + emit_fn(e,n,1); + if (n->text && strcmp(n->text,"main")==0) main_fn=n; + } + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) { + FeNode *m; + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) emit_fn(e,m,1); + } + fputc('\n',e->out); + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) emit_fn(e,n,0); + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) { + FeNode *m; + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) emit_fn(e,m,0); + } + if (main_fn) { fputc('\n',e->out); emit_main_wrapper(e,main_fn); } +} From 0dada80e65cd08a95a718d926c31569803f1bedf Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 21:54:22 +0900 Subject: [PATCH 076/184] test: unify registry and capture Watcom diagnostics --- src/ferrolang_vm/dosboxx.py | 22 ++++- src/ferrolang_vm/registry.py | 120 +++++++++++++++++++++++++ src/ferrolang_vm/registry_m1_m3.py | 69 -------------- src/ferrolang_vm/registry_m4_m6.py | 49 ---------- src/ferrolang_vm/suite.py | 9 +- tools/tests/test_milestones_dosboxx.py | 11 ++- 6 files changed, 149 insertions(+), 131 deletions(-) create mode 100644 src/ferrolang_vm/registry.py delete mode 100644 src/ferrolang_vm/registry_m1_m3.py delete mode 100644 src/ferrolang_vm/registry_m4_m6.py diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index 1deb9c6..581f14c 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -140,19 +140,29 @@ def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: for index, case in enumerate(cases): key = f"C{index:03d}" command = case.command + # Watcom writes diagnostics into the current directory. Isolate each + # case so a later pytest item never sees stale diagnostics. + lines.extend([ + "if exist *.ERR del *.ERR > NUL", + f"if exist RESULTS\\{key}.ERR del RESULTS\\{key}.ERR > NUL", + ]) if not trace_dos: command += f" > RESULTS\\{key}.LOG" lines.append(command) if case.expect_success: lines.extend([ - f"if errorlevel 1 goto {key}F", f"echo PASS>RESULTS\\{key}.RES", - f"goto {key}D", f":{key}F", f"echo FAIL>RESULTS\\{key}.RES", f":{key}D", + f"if errorlevel 1 goto {key}E", f"echo PASS>RESULTS\\{key}.RES", + f"goto {key}C", f":{key}E", f"echo FAIL>RESULTS\\{key}.RES", ]) else: lines.extend([ - f"if errorlevel 1 goto {key}P", f"echo FAIL>RESULTS\\{key}.RES", - f"goto {key}D", f":{key}P", f"echo PASS>RESULTS\\{key}.RES", f":{key}D", + f"if errorlevel 1 goto {key}E", f"echo FAIL>RESULTS\\{key}.RES", + f"goto {key}C", f":{key}E", f"echo PASS>RESULTS\\{key}.RES", ]) + lines.extend([ + f":{key}C", f"if exist *.ERR type *.ERR > RESULTS\\{key}.ERR", + f"if not exist RESULTS\\{key}.ERR type NUL > RESULTS\\{key}.ERR", + ]) lines.extend([ "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", "echo DONE>RUN.OK", *(["pause"] if show_dos else []), "exit", "", @@ -190,6 +200,10 @@ class SuiteRun: console = self.root / "CONSOLE.LOG" return console.read_text(encoding="utf-8", errors="replace") if console.is_file() else "" + def err(self, case: Case) -> str: + path = self.fec / "RESULTS" / f"{self._key(case)}.ERR" + return path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" + def cleanup(self) -> None: if not self.keep: shutil.rmtree(self.root, ignore_errors=True) diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py new file mode 100644 index 0000000..5ef627e --- /dev/null +++ b/src/ferrolang_vm/registry.py @@ -0,0 +1,120 @@ +"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" +from __future__ import annotations + +from .suite import Case + + +def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +CASES: list[Case] = [] +for _name in ("basic", "literals", "keybuilt", "v012form"): + CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) +for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): + CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) +for _name in ("misssemi", "unclcomm", "logical"): + CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) + +for _name in ("hello", "scopes"): + _upper = _name.upper() + CASES.extend([ + _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), + _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), + ]) +CASES.extend([ + _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), + _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), +]) +_m2_outputs = { + "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", + "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", + "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", +} +for _name, _output in _m2_outputs.items(): + CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) + +def _m3_runtime(name: str) -> list[Case]: + upper = name.upper() + return [ + _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), + _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), + ] + + +for _name in ("struct", "enum", "array", "mutable"): + CASES.extend(_m3_runtime(_name)) +for _name in ("bad-mlet", "bad-shwr"): + CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) +for _name in ("str", "for", "nested", "char", "arrayctx"): + CASES.extend(_m3_runtime(_name)) +for _name in ("bounds", "slcbound"): + _upper = _name.upper() + CASES.extend([ + _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), + _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), + ]) +CASES.extend([ + _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), + _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), +]) +for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): + CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) + +"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. + +Only commands and their expected status live here; the ``.fe`` fixtures stay +under ``fec/tests`` and are copied by the runner. +""" +def _c(milestone: int, name: str, command: str, ok: bool) -> Case: + return Case(f"m{milestone}-{name}", milestone, command, ok) + + +CASES.extend([ + _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), + _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), + _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), + _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), + _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), + _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), + _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), + _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), +]) + +_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} +for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): + _output = _m4_outputs.get(_name, _name.upper()) + CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) + +for _name in ("defer", "owned"): + CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) +for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): + _output = "BAD-DES" if _name == "bad-dest" else _name.upper() + CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " + f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) +CASES += [ + _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), + _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), + _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), +] + +for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): + CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) +for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): + CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) + + +def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: + if only is not None: + return [case for case in CASES if case.milestone == only] + return [case for case in CASES if case.milestone <= through] diff --git a/src/ferrolang_vm/registry_m1_m3.py b/src/ferrolang_vm/registry_m1_m3.py deleted file mode 100644 index a9623ab..0000000 --- a/src/ferrolang_vm/registry_m1_m3.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" -from __future__ import annotations - -from .suite import Case - - -def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: - return Case(f"m{milestone}-{name}", milestone, command, ok) - - -M1_M3_CASES: list[Case] = [] -for _name in ("basic", "literals", "keybuilt", "v012form"): - M1_M3_CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) -for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): - M1_M3_CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) -for _name in ("misssemi", "unclcomm", "logical"): - M1_M3_CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) - -for _name in ("hello", "scopes"): - _upper = _name.upper() - M1_M3_CASES.extend([ - _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), - ]) -M1_M3_CASES.extend([ - _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), -]) -_m2_outputs = { - "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", - "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", - "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", -} -for _name, _output in _m2_outputs.items(): - M1_M3_CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) - -def _m3_runtime(name: str) -> list[Case]: - upper = name.upper() - return [ - _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), - ] - - -for _name in ("struct", "enum", "array", "mutable"): - M1_M3_CASES.extend(_m3_runtime(_name)) -for _name in ("bad-mlet", "bad-shwr"): - M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) -for _name in ("str", "for", "nested", "char", "arrayctx"): - M1_M3_CASES.extend(_m3_runtime(_name)) -for _name in ("bounds", "slcbound"): - _upper = _name.upper() - M1_M3_CASES.extend([ - _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), - ]) -M1_M3_CASES.extend([ - _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), - _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), -]) -for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): - M1_M3_CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) diff --git a/src/ferrolang_vm/registry_m4_m6.py b/src/ferrolang_vm/registry_m4_m6.py deleted file mode 100644 index 48b23e0..0000000 --- a/src/ferrolang_vm/registry_m4_m6.py +++ /dev/null @@ -1,49 +0,0 @@ -"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. - -Only commands and their expected status live here; the ``.fe`` fixtures stay -under ``fec/tests`` and are copied by the runner. -""" -from __future__ import annotations - -from .suite import Case - - -def _c(milestone: int, name: str, command: str, ok: bool) -> Case: - return Case(f"m{milestone}-{name}", milestone, command, ok) - - -M4_M6_CASES: list[Case] = [ - _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), - _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), - _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), - _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), - _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), - _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), - _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), -] - -_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} -for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): - _output = _m4_outputs.get(_name, _name.upper()) - M4_M6_CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) - -for _name in ("defer", "owned"): - M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) -for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): - _output = "BAD-DES" if _name == "bad-dest" else _name.upper() - M4_M6_CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) -M4_M6_CASES += [ - _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), - _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), - _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), -] - -for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): - M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) -for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): - M4_M6_CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py index 7072f51..b402d4e 100644 --- a/src/ferrolang_vm/suite.py +++ b/src/ferrolang_vm/suite.py @@ -13,10 +13,5 @@ class Case: def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: - from .registry_m1_m3 import M1_M3_CASES - from .registry_m4_m6 import M4_M6_CASES - - cases = [*M1_M3_CASES, *M4_M6_CASES] - if only is not None: - return [case for case in cases if case.milestone == only] - return [case for case in cases if case.milestone <= through] + from .registry import all_cases as _all_cases + return _all_cases(through=through, only=only) diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py index c6bc2fd..a1c0ea1 100644 --- a/tools/tests/test_milestones_dosboxx.py +++ b/tools/tests/test_milestones_dosboxx.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import warnings import pytest @@ -48,7 +49,13 @@ def test_compiler_build(suite_run: SuiteRun) -> None: def test_milestone_case(case: Case, suite_run: SuiteRun) -> None: if suite_run.result() != "PASS": pytest.skip("compiler build failed") - assert suite_run.result(case) == "PASS", ( + result = suite_run.result(case) + err = suite_run.err(case) + if result == "PASS" and err: + warning_lines = [line for line in err.splitlines() if "warning" in line.lower()] + if warning_lines: + warnings.warn("\n".join(warning_lines), stacklevel=1) + assert result == "PASS", ( f"DOS command: {case.command}\nExpected success: {case.expect_success}\n" - f"{suite_run.log(case)}" + f"{suite_run.log(case)}\n{err}" ) From 1856e99a61daffcbf8bdc61bd7f7d77e900d8a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:54:50 +0900 Subject: [PATCH 077/184] wire M7 checker and emitter --- fec/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/Makefile b/fec/Makefile index a6eb4c5..21a8e3d 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -std=c89 CPPFLAGS ?= -Isrc -SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check.c src/lower.c src/emit_c.c src/driver.c +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check_m7.c src/lower.c src/emit_c_m7.c src/driver.c OBJ = $(SRC:.c=.o) .PHONY: all clean dos-build From a2d7dffcae80f1977872144028dd99aa667664be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:55:00 +0900 Subject: [PATCH 078/184] wire M7 DOS build --- fec/build-dos.bat | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/fec/build-dos.bat b/fec/build-dos.bat index 02425ec..ecde577 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -6,9 +6,6 @@ if exist BUILD.OK del BUILD.OK if exist BUILD.FAIL del BUILD.FAIL if exist fec.exe del fec.exe if exist __wcl__.lnk del __wcl__.lnk -rem WCL writes test objects into the current directory. Remove all prior build -rem artifacts before the wildcard link so 32-bit test objects cannot enter the -rem 16-bit compiler executable. if exist *.obj del *.obj if "%WATCOM%"=="" set WATCOM=C:\DEVEL\WATCOMC @@ -30,17 +27,15 @@ wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=m7.obj src\m7.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=own.obj src\own.c if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check.c +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check_m7.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lower.obj src\lower.c if errorlevel 1 goto build_fail -rem Use an unambiguous short object name for the emit_c source. -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emit_c.c +rem Use an unambiguous short object name for the M7 emitter source. +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emit_c_m7.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=driver.obj src\driver.c if errorlevel 1 goto build_fail -rem DOS command lines are limited to roughly 126 characters. All stale objects -rem were removed above, so the wildcard contains exactly this build's objects. wcl -q -za -wx -bt=dos -ml -k32768 -fe=fec.exe *.obj if errorlevel 1 goto build_fail if not exist fec.exe goto build_fail From 53f4d79c4f3dde95e1e08f81f37024c1011fd6be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:55:28 +0900 Subject: [PATCH 079/184] expose ownership flags to M7 emitter --- fec/src/emit_c.h | 1 + 1 file changed, 1 insertion(+) diff --git a/fec/src/emit_c.h b/fec/src/emit_c.h index 10634a8..8be10e6 100644 --- a/fec/src/emit_c.h +++ b/fec/src/emit_c.h @@ -2,6 +2,7 @@ #define FE_EMIT_C_H #include "check.h" +#include "own.h" typedef struct FeEmitter { FILE *out; From 594a837003fb8355173fe6c5ac038cc2b22e0264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:55:41 +0900 Subject: [PATCH 080/184] ci: smoke test M7 compiler branch --- .github/workflows/m7-smoke.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/m7-smoke.yml diff --git a/.github/workflows/m7-smoke.yml b/.github/workflows/m7-smoke.yml new file mode 100644 index 0000000..3153031 --- /dev/null +++ b/.github/workflows/m7-smoke.yml @@ -0,0 +1,30 @@ +name: m7-smoke +on: + push: + branches: [agent/m7-complete] + workflow_dispatch: + +jobs: + host-smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build compiler as C89 + run: make -C fec clean all CFLAGS='-O0 -Wall -Wextra -Werror -std=c89' + - name: Check M7 pass fixtures + shell: bash + run: | + set -euo pipefail + for f in fec/tests/m7/ok*.fe; do + fec/fec --target=bits32 --check "$f" + done + - name: Check M7 fail fixtures + shell: bash + run: | + set -euo pipefail + for f in fec/tests/m7/bad*.fe; do + if fec/fec --target=bits32 --check "$f"; then + echo "expected failure: $f" + exit 1 + fi + done From bdb307ffcc9bfa63c950b66df565a25ed6c6574d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:56:18 +0900 Subject: [PATCH 081/184] ci: focus M7 smoke on build errors --- .github/workflows/m7-smoke.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/m7-smoke.yml b/.github/workflows/m7-smoke.yml index 3153031..fcab7f7 100644 --- a/.github/workflows/m7-smoke.yml +++ b/.github/workflows/m7-smoke.yml @@ -10,7 +10,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Build compiler as C89 - run: make -C fec clean all CFLAGS='-O0 -Wall -Wextra -Werror -std=c89' + run: make -C fec clean all CFLAGS='-O0 -Wall -Wextra -std=c89' - name: Check M7 pass fixtures shell: bash run: | From 7d7616717998abd087b7d7ed5e14856fc9e40a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 21:57:34 +0900 Subject: [PATCH 082/184] document M7 smoke coverage --- fec/tests/m7/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/tests/m7/README.md b/fec/tests/m7/README.md index 3679b4b..abfce9f 100644 --- a/fec/tests/m7/README.md +++ b/fec/tests/m7/README.md @@ -3,6 +3,6 @@ M7 adds optionals and error unions on top of the M6 ownership model. `ok*.fe` must compile. `bad*.fe` must fail according to the first-line error marker. -The files are not wired into `TEST-DOS.BAT` until M7 work begins. +The branch-level `m7-smoke` workflow builds the compiler as C89 and checks every M7 pass/fail fixture. Formal regression remains the FreeDOS/Open Watcom path. Coverage: contextual `null`, `?T`, `.?`, `Some`/`None` pattern-only non-destructive views, `mem.replace` extraction, lazy `orelse`/`catch`, nominal error unions, `try`, block/short `catch`, and error code/name uniqueness, plus R4/R7 interactions with optional references/owners. From abbfdb3092e16738898be182806f6e53639b5b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 22:00:59 +0900 Subject: [PATCH 083/184] remove temporary M7 smoke workflow --- .github/workflows/m7-smoke.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/m7-smoke.yml diff --git a/.github/workflows/m7-smoke.yml b/.github/workflows/m7-smoke.yml deleted file mode 100644 index fcab7f7..0000000 --- a/.github/workflows/m7-smoke.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: m7-smoke -on: - push: - branches: [agent/m7-complete] - workflow_dispatch: - -jobs: - host-smoke: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Build compiler as C89 - run: make -C fec clean all CFLAGS='-O0 -Wall -Wextra -std=c89' - - name: Check M7 pass fixtures - shell: bash - run: | - set -euo pipefail - for f in fec/tests/m7/ok*.fe; do - fec/fec --target=bits32 --check "$f" - done - - name: Check M7 fail fixtures - shell: bash - run: | - set -euo pipefail - for f in fec/tests/m7/bad*.fe; do - if fec/fec --target=bits32 --check "$f"; then - echo "expected failure: $f" - exit 1 - fi - done From 24a99abd37501a46c40c0308ad5804590089c39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 22:01:07 +0900 Subject: [PATCH 084/184] restore M7 fixture README --- fec/tests/m7/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/tests/m7/README.md b/fec/tests/m7/README.md index abfce9f..3679b4b 100644 --- a/fec/tests/m7/README.md +++ b/fec/tests/m7/README.md @@ -3,6 +3,6 @@ M7 adds optionals and error unions on top of the M6 ownership model. `ok*.fe` must compile. `bad*.fe` must fail according to the first-line error marker. -The branch-level `m7-smoke` workflow builds the compiler as C89 and checks every M7 pass/fail fixture. Formal regression remains the FreeDOS/Open Watcom path. +The files are not wired into `TEST-DOS.BAT` until M7 work begins. Coverage: contextual `null`, `?T`, `.?`, `Some`/`None` pattern-only non-destructive views, `mem.replace` extraction, lazy `orelse`/`catch`, nominal error unions, `try`, block/short `catch`, and error code/name uniqueness, plus R4/R7 interactions with optional references/owners. From f946b14bded97db4a1a3bff33bd38a562f6e5a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Sun, 16 Aug 2026 22:09:04 +0900 Subject: [PATCH 085/184] fix: keep M7 return context through if branches --- fec/src/check_m7.c | 84 ++++++++++++++++++++++------------------------ 1 file changed, 41 insertions(+), 43 deletions(-) diff --git a/fec/src/check_m7.c b/fec/src/check_m7.c index 5af921f..193ecbf 100644 --- a/fec/src/check_m7.c +++ b/fec/src/check_m7.c @@ -866,53 +866,51 @@ static void m7_check_stmt(FeCheckerState *s, FeNode *n) m7_check_if_let(s,n); else { FeType *cond; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; cond=m7_check_expr(s,n->a); if (known(cond) && cond->kind!=FE_TYPE_BOOL) err(s->c,n->loc,"if condition must be bool"); - /* Reuse the verified branch merge by letting the M6 statement - machinery handle branches that contain no M7-only nodes. */ - if (!m7_node_feature(n->b) && !m7_node_feature(n->c)) - check_stmt(s,n); - else { - FeFlowSlot base[FE_M7_FLOW_CAP]; - FeFlowSlot left[FE_M7_FLOW_CAP]; - FeFlowSlot right[FE_M7_FLOW_CAP]; - FeOwnState *own_base; - FeOwnState *own_left; - FeOwnState *own_right; - FeFlowBorrow *borrow_base; - FeFlowBorrow *borrow_left; - FeFlowBorrow *borrow_right; - unsigned count; - unsigned i; - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - m7_check_stmt(s,n->b); - flow_capture(s->scope,left,count); - own_left=flow_own_new(s,count); - borrow_left=flow_borrow_new(s,count); - flow_own_capture(left,own_left,count); - flow_borrow_capture(left,borrow_left,count); - m7_restore_flow(base,own_base,borrow_base,count); - if (n->c) m7_check_stmt(s,n->c); - if (n->c) { - flow_capture(s->scope,right,count); - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - flow_own_capture(right,own_right,count); - flow_borrow_capture(right,borrow_right,count); - } else { - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - for (i=0;ib); + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + m7_restore_flow(base,own_base,borrow_base,count); + if (n->c) m7_check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + } else { + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + for (i=0;i Date: Sun, 16 Aug 2026 22:10:46 +0900 Subject: [PATCH 086/184] refactor: rebuild the milestone registry as a table registry.py was literally `cat registry_m1_m3.py registry_m4_m6.py`, which left a duplicate `_c` definition and a stray module-level docstring at the seam. Replace the hand-rolled append loops with five builders -- _emit, _wcl, _triple, _rejects, _dump_ast -- and express the cases as one ordered list. The irregularities are now parameters instead of one-off code, each with the reason recorded: - _triple(stem=) for M2 castwhil emitting CAST16 - _triple(build_source=) for M4 prop, which emits PROP.C but compiles PROPTEST.C because that file #includes it - _triple(run_suffix="trap", run_ok=False) for the M3 bounds cases - _triple(emit_suffix=None) for M4, whose ids lack the -emit suffix - _emit(output_first=True) for M6, which passes -o before the input The three scattered 8.3 output-name mappings collapse into one _OUT83 table keyed by (milestone, name). The key needs both: bad-type is BAD-TY in M2 but BAD-TYP in M4, and bad-cond is BAD-CO in M2 but unshortened in M5. Values are carried over verbatim -- the shortenings are inconsistent and several were never required, but that is a separate decision. suite.py keeps only Case and drops the all_cases forwarder, so the registry -> suite -> registry cycle is gone along with the function-scoped import that worked around it. dosboxx.py still imports Case from suite and is untouched. Verified behaviour-preserving by snapshotting (id, milestone, command, expect_success) for all 150 cases in order before and after: diff is empty. pytest collects the same 151 items. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- src/ferrolang_vm/registry.py | 265 ++++++++++++++++--------- src/ferrolang_vm/suite.py | 11 +- tools/tests/test_milestones_dosboxx.py | 3 +- 3 files changed, 176 insertions(+), 103 deletions(-) diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index 5ef627e..99eecc4 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -1,119 +1,192 @@ -"""Explicit M1--M3 commands and expectations from TEST-DOS.BAT.""" +"""Milestone case registry: DOS commands and their expected exit status. + +Only commands live here; the ``.fe`` fixtures stay under ``fec/tests`` and are +copied into the disposable DOS filesystem by the runner. Case order is load +bearing -- ``emit`` must precede ``build`` must precede ``run`` for the same +fixture, because each step consumes the previous step's output. +""" from __future__ import annotations from .suite import Case +PASS = "TESTS\\PASS" +FAIL = "TESTS\\FAIL" +STD = "STD" +M2 = "TESTS\\M2" +M3 = "TESTS\\M3" +M4 = "TESTS\\M4" +M5 = "TESTS\\M5" +M6 = "TESTS\\M6" +OUT = "OUT" -def _c(milestone: int, name: str, command: str, ok: bool = True) -> Case: +# Emitted-C basenames that were hand-shortened for DOS 8.3. Keyed by milestone +# because the same fixture name maps to different outputs across milestones +# (``bad-type`` is BAD-TY in M2 but BAD-TYP in M4). The shortenings are not +# consistent -- M2 cut to six characters, M4 to seven, and several were never +# required at all since BAD-COND is already a legal 8.3 name. Preserved verbatim; +# changing one renames a file inside the DOS run, so re-verify if you touch it. +_OUT83 = { + (2, "bad-cond"): "BAD-CO", + (2, "bad-cast"): "BAD-CA", + (2, "bad-asgn"): "BAD-AS", + (2, "bad-unk"): "BAD-UN", + (2, "bad-ari"): "BAD-AR", + (2, "bad-type"): "BAD-TY", + (2, "bad-ret"): "BAD-RE", + (2, "bad-unit"): "BAD-UI", + (2, "bad-void"): "BAD-VO", + (4, "bad-type"): "BAD-TYP", + (4, "bad-writ"): "BAD-WRI", + (5, "bad-dest"): "BAD-DES", +} + + +def _case(milestone: int, name: str, command: str, ok: bool = True) -> Case: return Case(f"m{milestone}-{name}", milestone, command, ok) -CASES: list[Case] = [] -for _name in ("basic", "literals", "keybuilt", "v012form"): - CASES.append(_c(1, f"{_name}-parse", f"FEC.EXE --dump-ast TESTS\\PASS\\{_name.upper()}.FE")) -for _name in ("core", "fmt", "io", "list", "map", "mem", "str", "sys"): - CASES.append(_c(1, f"std-{_name}-parse", f"FEC.EXE --dump-ast STD\\{_name.upper()}.FE")) -for _name in ("misssemi", "unclcomm", "logical"): - CASES.append(_c(1, f"{_name}-reject", f"FEC.EXE --dump-ast TESTS\\FAIL\\{_name.upper()}.FE", False)) +def _fe(directory: str, name: str) -> str: + return f"{directory}\\{name.upper()}.FE" -for _name in ("hello", "scopes"): - _upper = _name.upper() - CASES.extend([ - _c(2, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M2\\{_upper}.FE -o TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M2\\{_upper}.EXE TESTS\\M2\\{_upper}.C"), - _c(2, f"{_name}-run", f"TESTS\\M2\\{_upper}.EXE"), - ]) -CASES.extend([ - _c(2, "castwhil-emit", "FEC.EXE --target=bits16 --emit-c TESTS\\M2\\CASTWHIL.FE -o TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-build", "WCL -q -za -bt=dos -fe=TESTS\\M2\\CAST16.EXE TESTS\\M2\\CAST16.C"), - _c(2, "castwhil-run", "TESTS\\M2\\CAST16.EXE"), -]) -_m2_outputs = { - "bad-cond": "BAD-CO", "bad-cast": "BAD-CA", "bad-asgn": "BAD-AS", - "bad-unk": "BAD-UN", "bad-ari": "BAD-AR", "bad-type": "BAD-TY", - "bad-ret": "BAD-RE", "bad-unit": "BAD-UI", "bad-void": "BAD-VO", -} -for _name, _output in _m2_outputs.items(): - CASES.append(_c(2, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M2\\{_name.upper()}.FE -o TESTS\\M2\\{_output}.C", False)) -def _m3_runtime(name: str) -> list[Case]: - upper = name.upper() +def _emit(source: str, output: str, *, target: str = "bits32", + flags: tuple[str, ...] = (), output_first: bool = False) -> str: + """``fec`` invocation that translates ``source`` to C at ``output``. + + ``output_first`` reproduces the M6 cases, which pass ``-o`` before the input + file while every other milestone passes it after. + """ + parts = ["FEC.EXE", f"--target={target}", *flags, "--emit-c"] + parts += ["-o", output, source] if output_first else [source, "-o", output] + return " ".join(parts) + + +def _wcl(exe: str, *sources: str, bits: int = 32, strict: bool = False, + defines: tuple[str, ...] = ()) -> str: + """Open Watcom invocation. ``strict`` is the M4 ``-wx -wcd=202`` pairing: + warnings are errors except W202, which the generated C trips on unused + helpers (see AGENTS.md).""" + parts = ["WCL386" if bits == 32 else "WCL", "-q", "-za"] + if strict: + parts += ["-wx", "-wcd=202"] + parts += ["-bt=dos", *defines, f"-fe={exe}", *sources] + return " ".join(parts) + + +def _dump_ast(milestone: int, directory: str, names: tuple[str, ...], *, + suffix: str, ok: bool = True, prefix: str = "") -> list[Case]: return [ - _c(3, f"{name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{upper}.FE -o TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{upper}.EXE TESTS\\M3\\{upper}.C"), - _c(3, f"{name}-run", f"TESTS\\M3\\{upper}.EXE"), + _case(milestone, f"{prefix}{name}-{suffix}", + f"FEC.EXE --dump-ast {_fe(directory, name)}", ok) + for name in names ] -for _name in ("struct", "enum", "array", "mutable"): - CASES.extend(_m3_runtime(_name)) -for _name in ("bad-mlet", "bad-shwr"): - CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) -for _name in ("str", "for", "nested", "char", "arrayctx"): - CASES.extend(_m3_runtime(_name)) -for _name in ("bounds", "slcbound"): - _upper = _name.upper() - CASES.extend([ - _c(3, f"{_name}-emit", f"FEC.EXE --target=bits32 --emit-c TESTS\\M3\\{_upper}.FE -o TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-build", f"WCL386 -q -za -bt=dos -fe=TESTS\\M3\\{_upper}.EXE TESTS\\M3\\{_upper}.C"), - _c(3, f"{_name}-trap", f"TESTS\\M3\\{_upper}.EXE", False), - ]) -CASES.extend([ - _c(3, "bounds-no-checks-emit", "FEC.EXE --target=bits32 --no-checks --emit-c TESTS\\M3\\BOUNDS.FE -o TESTS\\M3\\BOUNDS-N.C"), - _c(3, "bounds-no-checks-build", "WCL386 -q -za -bt=dos -fe=TESTS\\M3\\BOUNDS-N.EXE TESTS\\M3\\BOUNDS-N.C"), -]) -for _name in ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"): - CASES.append(_c(3, f"{_name}-reject", "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M3\\{_name.upper()}.FE -o TESTS\\M3\\{_name.upper()}.C", False)) - -"""Explicit M4--M5 cases from TEST-DOS.BAT and M6 fixture expectations. - -Only commands and their expected status live here; the ``.fe`` fixtures stay -under ``fec/tests`` and are copied by the runner. -""" -def _c(milestone: int, name: str, command: str, ok: bool) -> Case: - return Case(f"m{milestone}-{name}", milestone, command, ok) +def _rejects(milestone: int, directory: str, names: tuple[str, ...], *, + suffix: str = "") -> list[Case]: + """Fixtures that must fail to compile. The emitted-C path is still spelled + out because ``fec`` needs an ``-o`` even when it is expected to bail.""" + return [ + _case(milestone, f"{name}-{suffix}" if suffix else name, + _emit(_fe(directory, name), + f"{directory}\\{_OUT83.get((milestone, name), name.upper())}.C"), + False) + for name in names + ] -CASES.extend([ - _c(4, "format", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\FORMAT.FE -o TESTS\\M4\\FORMAT.C", True), - _c(4, "format-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\FORMAT.EXE TESTS\\M4\\FORMAT.C", True), - _c(4, "format-run", "TESTS\\M4\\FORMAT.EXE", True), - _c(4, "try-fpr", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\TRY-FPR.FE -o TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\TRY-FPR.EXE TESTS\\M4\\TRY-FPR.C", True), - _c(4, "try-fpr-run", "TESTS\\M4\\TRY-FPR.EXE", True), - _c(4, "prop", "FEC.EXE --target=bits32 --emit-c TESTS\\M4\\PROP.FE -o TESTS\\M4\\PROP.C", True), - _c(4, "prop-build", "WCL386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\\M4\\PROP.EXE TESTS\\M4\\PROPTEST.C", True), - _c(4, "prop-run", "TESTS\\M4\\PROP.EXE", True), -]) +def _triple(milestone: int, name: str, directory: str, *, stem: str | None = None, + target: str = "bits32", bits: int = 32, strict: bool = False, + build_source: str | None = None, emit_suffix: str | None = "emit", + run_suffix: str = "run", run_ok: bool = True) -> list[Case]: + """emit -> build -> run for one fixture. -_m4_outputs = {"bad-type": "BAD-TYP", "bad-writ": "BAD-WRI"} -for _name in ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls"): - _output = _m4_outputs.get(_name, _name.upper()) - CASES.append(_c(4, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M4\\{_name.upper()}.FE -o TESTS\\M4\\{_output}.C", False)) + ``stem`` renames the C/EXE pair when the fixture name does not fit 8.3 or + collides (M2 castwhil emits CAST16). ``build_source`` compiles a different + file than the one emitted (M4 prop emits PROP.C but builds PROPTEST.C, which + ``#include``s it). + """ + stem = stem or name.upper() + cfile = f"{directory}\\{stem}.C" + exe = f"{directory}\\{stem}.EXE" + emit_id = f"{name}-{emit_suffix}" if emit_suffix else name + return [ + _case(milestone, emit_id, _emit(_fe(directory, name), cfile, target=target)), + _case(milestone, f"{name}-build", + _wcl(exe, build_source or cfile, bits=bits, strict=strict)), + _case(milestone, f"{name}-{run_suffix}", exe, run_ok), + ] -for _name in ("defer", "owned"): - CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_name.upper()}.C", True)) -for _name in ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop"): - _output = "BAD-DES" if _name == "bad-dest" else _name.upper() - CASES.append(_c(5, _name, "FEC.EXE --target=bits32 --emit-c " - f"TESTS\\M5\\{_name.upper()}.FE -o TESTS\\M5\\{_output}.C", False)) -CASES += [ - _c(5, "runtime", "FEC.EXE --target=bits32 --emit-c TESTS\\M5\\RUNTIME.FE -o TESTS\\M5\\RUNT-G.C", True), - _c(5, "runtime-build", "WCL386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\\M5\\RUNTIME.EXE TESTS\\M5\\RUNT-G.C TESTS\\M5\\RUNTIME.C", True), - _c(5, "runtime-run", "TESTS\\M5\\RUNTIME.EXE", True), + +CASES: list[Case] = [ + # -- M1: parse only ------------------------------------------------------- + *_dump_ast(1, PASS, ("basic", "literals", "keybuilt", "v012form"), suffix="parse"), + *_dump_ast(1, STD, ("core", "fmt", "io", "list", "map", "mem", "str", "sys"), + suffix="parse", prefix="std-"), + *_dump_ast(1, FAIL, ("misssemi", "unclcomm", "logical"), suffix="reject", ok=False), + + # -- M2: first generated C ------------------------------------------------ + *_triple(2, "hello", M2), + *_triple(2, "scopes", M2), + *_triple(2, "castwhil", M2, stem="CAST16", target="bits16", bits=16), + *_rejects(2, M2, ("bad-cond", "bad-cast", "bad-asgn", "bad-unk", "bad-ari", + "bad-type", "bad-ret", "bad-unit", "bad-void"), suffix="reject"), + + # -- M3: aggregates, strings, bounds checks ------------------------------- + *_triple(3, "struct", M3), + *_triple(3, "enum", M3), + *_triple(3, "array", M3), + *_triple(3, "mutable", M3), + *_rejects(3, M3, ("bad-mlet", "bad-shwr"), suffix="reject"), + *_triple(3, "str", M3), + *_triple(3, "for", M3), + *_triple(3, "nested", M3), + *_triple(3, "char", M3), + *_triple(3, "arrayctx", M3), + # These two must trap at runtime: the bounds check is the feature under test. + *_triple(3, "bounds", M3, run_suffix="trap", run_ok=False), + *_triple(3, "slcbound", M3, run_suffix="trap", run_ok=False), + _case(3, "bounds-no-checks-emit", + _emit(_fe(M3, "bounds"), f"{M3}\\BOUNDS-N.C", flags=("--no-checks",))), + _case(3, "bounds-no-checks-build", + _wcl(f"{M3}\\BOUNDS-N.EXE", f"{M3}\\BOUNDS-N.C")), + *_rejects(3, M3, ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", + "badfield", "badindex"), suffix="reject"), + + # -- M4: formatting and error propagation --------------------------------- + *_triple(4, "format", M4, strict=True, emit_suffix=None), + *_triple(4, "try-fpr", M4, strict=True, emit_suffix=None), + *_triple(4, "prop", M4, strict=True, emit_suffix=None, + build_source=f"{M4}\\PROPTEST.C"), + *_rejects(4, M4, ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", + "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls")), + + # -- M5: defer and ownership ---------------------------------------------- + _case(5, "defer", _emit(_fe(M5, "defer"), f"{M5}\\DEFER.C")), + _case(5, "owned", _emit(_fe(M5, "owned"), f"{M5}\\OWNED.C")), + *_rejects(5, M5, ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", + "bad-proj", "bad-clos", "bad-loop")), + # The runtime case links the generated C against a hand-written allocator + # shim, so malloc/free are redirected at compile time. + _case(5, "runtime", _emit(_fe(M5, "runtime"), f"{M5}\\RUNT-G.C")), + _case(5, "runtime-build", + _wcl(f"{M5}\\RUNTIME.EXE", f"{M5}\\RUNT-G.C", f"{M5}\\RUNTIME.C", + defines=("-dmalloc=m5_malloc", "-dfree=m5_free"))), + _case(5, "runtime-run", f"{M5}\\RUNTIME.EXE"), + + # -- M6: borrow checking (R1--R8) ----------------------------------------- + *[_case(6, name, f"FEC.EXE --check {_fe(M6, name)}", False) for name in ( + "badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", + "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", + "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", + "badtwo", "badup", "badweak")], + *[_case(6, name, _emit(_fe(M6, name), f"{OUT}\\{name.upper()}.C", + output_first=True)) for name in ( + "okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", + "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", + "okstatic", "oktemp", "oktrim", "okwcall")], ] -for _name in ("badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak"): - CASES.append(_c(6, _name, f"FEC.EXE --check TESTS\\M6\\{_name.upper()}.FE", False)) -for _name in ("okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall"): - CASES.append(_c(6, _name, f"FEC.EXE --target=bits32 --emit-c -o OUT\\{_name.upper()}.C TESTS\\M6\\{_name.upper()}.FE", True)) - - def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: if only is not None: return [case for case in CASES if case.milestone == only] diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py index b402d4e..b412298 100644 --- a/src/ferrolang_vm/suite.py +++ b/src/ferrolang_vm/suite.py @@ -1,4 +1,8 @@ -"""Shared types and selection for the explicit milestone registries.""" +"""The one type shared by the case registry and the DOSBox-X runner. + +Kept separate from ``registry`` so that ``dosboxx`` can depend on the type +without importing the case data. +""" from __future__ import annotations from dataclasses import dataclass @@ -10,8 +14,3 @@ class Case: milestone: int command: str expect_success: bool - - -def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: - from .registry import all_cases as _all_cases - return _all_cases(through=through, only=only) diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py index a1c0ea1..32d2a01 100644 --- a/tools/tests/test_milestones_dosboxx.py +++ b/tools/tests/test_milestones_dosboxx.py @@ -6,7 +6,8 @@ import warnings import pytest from ferrolang_vm.dosboxx import SuiteRun, run_suite -from ferrolang_vm.suite import Case, all_cases +from ferrolang_vm.registry import all_cases +from ferrolang_vm.suite import Case def _number(name: str) -> int: From 4ad3e3097bd60d37f2fdbe193e1d6c47a9a02947 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 22:11:36 +0900 Subject: [PATCH 087/184] refactor: derive the milestone bounds from the registry The highest supported milestone was spelled out in six places across four files: range(1, 7) and default="m6" in test_cli.py, the same pair in test_milestones_dosboxx.py, and through=6 in both registry.py and suite.py. Registering M7 meant finding all six, and missing one failed silently. Derive MAX_MILESTONE and MILESTONES from CASES instead, and move the mN selector parser to registry.milestone_number so the pytest module stops carrying its own copy. Adding cases for a new milestone is now enough for ferro-test to accept --through/--only for it. No behaviour change: MAX_MILESTONE evaluates to 6, ferro-test still advertises {m1..m6} with default m6, and the case snapshot is unchanged. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- src/ferrolang_vm/registry.py | 17 ++++++++++++++++- src/ferrolang_vm/test_cli.py | 9 ++++----- tools/tests/test_milestones_dosboxx.py | 16 +++------------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index 99eecc4..5478bbc 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -187,7 +187,22 @@ CASES: list[Case] = [ "okstatic", "oktemp", "oktrim", "okwcall")], ] -def all_cases(*, through: int = 6, only: int | None = None) -> list[Case]: +MAX_MILESTONE: int = max(case.milestone for case in CASES) +MILESTONES: tuple[str, ...] = tuple(f"m{number}" + for number in range(1, MAX_MILESTONE + 1)) + + +def milestone_number(name: str) -> int: + """Parse an ``mN`` selector against the milestones the registry knows about.""" + if not name.startswith("m") or not name[1:].isdigit(): + raise ValueError(f"invalid milestone: {name}") + value = int(name[1:]) + if value not in range(1, MAX_MILESTONE + 1): + raise ValueError(f"unsupported milestone: {name}") + return value + + +def all_cases(*, through: int = MAX_MILESTONE, only: int | None = None) -> list[Case]: if only is not None: return [case for case in CASES if case.milestone == only] return [case for case in CASES if case.milestone <= through] diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py index 8a9d1e3..e20f744 100644 --- a/src/ferrolang_vm/test_cli.py +++ b/src/ferrolang_vm/test_cli.py @@ -7,9 +7,7 @@ import sys from pathlib import Path from .dosboxx import DosboxError, setup - - -MILESTONES = tuple(f"m{number}" for number in range(1, 7)) +from .registry import MAX_MILESTONE, MILESTONES def main() -> int: @@ -23,8 +21,9 @@ def main() -> int: help="confirm acceptance of the Sybase Open Watcom Public License") run = commands.add_parser("run", help="build once and run milestone pytest cases") selection = run.add_mutually_exclusive_group() - selection.add_argument("--through", choices=MILESTONES, default="m6", - help="run cumulatively through this milestone (default: m6)") + selection.add_argument("--through", choices=MILESTONES, default=f"m{MAX_MILESTONE}", + help="run cumulatively through this milestone " + f"(default: m{MAX_MILESTONE})") selection.add_argument("--only", choices=MILESTONES, help="run only this milestone's cases") run.add_argument("-v", "--verbose", action="store_true", help="show every pytest case") diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py index 32d2a01..c386b62 100644 --- a/tools/tests/test_milestones_dosboxx.py +++ b/tools/tests/test_milestones_dosboxx.py @@ -6,23 +6,13 @@ import warnings import pytest from ferrolang_vm.dosboxx import SuiteRun, run_suite -from ferrolang_vm.registry import all_cases +from ferrolang_vm.registry import MAX_MILESTONE, all_cases, milestone_number from ferrolang_vm.suite import Case - -def _number(name: str) -> int: - if not name.startswith("m") or not name[1:].isdigit(): - raise ValueError(f"invalid milestone: {name}") - value = int(name[1:]) - if value not in range(1, 7): - raise ValueError(f"unsupported milestone: {name}") - return value - - ONLY = os.environ.get("FERRO_TEST_ONLY") CASES = all_cases( - through=_number(os.environ.get("FERRO_TEST_THROUGH", "m6")), - only=_number(ONLY) if ONLY else None, + through=milestone_number(os.environ.get("FERRO_TEST_THROUGH", f"m{MAX_MILESTONE}")), + only=milestone_number(ONLY) if ONLY else None, ) From 594704a07d2757941a3d84047ab3db82d5dc711b Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 22:14:50 +0900 Subject: [PATCH 088/184] dev: promote DOSBox-X and remove QEMU support --- .gitignore | 21 +- .qemu/install.ps1 | 28 -- .qemu/readme.txt | 144 --------- .qemu/send-keys.ps1 | 60 ---- .qemu/setup.ps1 | 26 -- AGENTS.md | 65 ++-- TODO.md | 27 -- fec/vm-m1.bat | 201 ------------ pyproject.toml | 6 +- src/ferrolang_vm/__init__.py | 2 +- src/ferrolang_vm/cli.py | 195 ------------ src/ferrolang_vm/daemon.py | 467 --------------------------- src/ferrolang_vm/dos_cli.py | 80 +++++ src/ferrolang_vm/dosboxx.py | 33 +- src/ferrolang_vm/paths.py | 5 + tools/README.md | 111 +++---- tools/qemu_ocr.py | 79 ----- tools/tcpagent/BUILD.BAT | 6 - tools/tcpagent/INSTALL.BAT | 15 - tools/tcpagent/Makefile | 38 --- tools/tcpagent/README.md | 101 ------ tools/tcpagent/REBUILD.BAT | 22 -- tools/tcpagent/WPP.RSP | 11 - tools/tcpagent/tcpagent.cfg | 12 - tools/tcpagent/tcpagent.cpp | 303 ------------------ uv.lock | 599 +---------------------------------- 26 files changed, 176 insertions(+), 2481 deletions(-) delete mode 100644 .qemu/install.ps1 delete mode 100644 .qemu/readme.txt delete mode 100644 .qemu/send-keys.ps1 delete mode 100644 .qemu/setup.ps1 delete mode 100644 TODO.md delete mode 100644 fec/vm-m1.bat delete mode 100644 src/ferrolang_vm/cli.py delete mode 100644 src/ferrolang_vm/daemon.py create mode 100644 src/ferrolang_vm/dos_cli.py create mode 100644 src/ferrolang_vm/paths.py delete mode 100644 tools/qemu_ocr.py delete mode 100644 tools/tcpagent/BUILD.BAT delete mode 100644 tools/tcpagent/INSTALL.BAT delete mode 100644 tools/tcpagent/Makefile delete mode 100644 tools/tcpagent/README.md delete mode 100644 tools/tcpagent/REBUILD.BAT delete mode 100644 tools/tcpagent/WPP.RSP delete mode 100644 tools/tcpagent/tcpagent.cfg delete mode 100644 tools/tcpagent/tcpagent.cpp diff --git a/.gitignore b/.gitignore index e699b19..6979fd4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,9 @@ -# QEMU runtime state, downloaded media, and generated captures -.qemu/* -!.qemu/*.ps1 -!.qemu/*.mjs -!.qemu/*.txt -!.qemu/share/ -!.qemu/share/*.c -.qemu/share/fec/ +# Legacy local QEMU state is no longer used, but may contain large user-owned images. +.qemu/ # Reproducible DOSBox-X/Open Watcom development cache and ephemeral runs .dosboxx/ -.qemu/*.qcow2 -.qemu/*.img -.qemu/*.iso -.qemu/*.zip -.qemu/*.png -.qemu/*.ppm -.qemu/*.tmp - # Windows reserved-device artifact: `> nul` under Git Bash creates a real file. # Committing it breaks checkout on Windows. nul @@ -39,6 +25,3 @@ __pycache__/ node_modules/ .npm/ .cache/ - -# Temporary DOS build sources -.qemu/share/dosag*.c diff --git a/.qemu/install.ps1 b/.qemu/install.ps1 deleted file mode 100644 index 2d3ade9..0000000 --- a/.qemu/install.ps1 +++ /dev/null @@ -1,28 +0,0 @@ -param() - -$ErrorActionPreference = 'Stop' -$qemu = Get-Command qemu-system-i386.exe -ErrorAction Stop -$disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' - -if (-not (Test-Path -LiteralPath $disk)) { - throw "Missing $disk. Create it first with: qemu-img create -f qcow2 .qemu\\freedos.qcow2 2G" -} -if (-not (Test-Path -LiteralPath $iso)) { - throw "Missing $iso. Run .qemu\\setup.ps1 first." -} -if (Get-NetTCPConnection -State Listen -LocalPort 4444 -ErrorAction SilentlyContinue) { - throw 'QEMU monitor port 4444 is already in use. Stop the existing VM first.' -} - -& $qemu.Source ` - -machine pc,usb=on ` - -cpu pentium3 ` - -m 64 ` - -drive "file=$disk,format=qcow2,if=ide,index=0,media=disk" ` - -drive "file=$iso,media=cdrom,readonly=on" ` - -nic user,model=ne2k_isa ` - -monitor "tcp:127.0.0.1:4444,server=on,wait=off" ` - -serial null ` - -boot order=d ` - -display default diff --git a/.qemu/readme.txt b/.qemu/readme.txt deleted file mode 100644 index bd4facd..0000000 --- a/.qemu/readme.txt +++ /dev/null @@ -1,144 +0,0 @@ -############################################################################### - FreeDOS 1.4 ("FreeDOS 1.4") -############################################################################### - - -------------------------------------------------------------------------------- - General system requirements: -------------------------------------------------------------------------------- - - * DOS-compatible system (Intel + BIOS, or UEFI with Legacy support) - - * At least 20MB free disk space: - - 20MB Plain DOS system - 30MB Plain DOS system, with sources - - 275MB Full installation including applications and games - 450MB Full installation with sources - - -------------------------------------------------------------------------------- - What's in all those zip files? -------------------------------------------------------------------------------- - -FD14-LiveCD.zip - - * FD14BOOT.IMG - Basic FreeDOS installation boot floppy image. - If your computer has a CD-ROM drive, but you cannot boot from the Live CD - or Legacy CD. Use this diskette image to boot the system. Then insert the - install CD. The FreeDOS installer should do the rest. This diskette - image is for installation purposes only and does not provide a Live - Environment. - - * FD14LIVE.ISO - The FreeDOS 1.4 installer. Most users should - use this image to install FreeDOS. - - Depending on your computer system and hardware configuration, you - can also use the LiveCD to boot and run FreeDOS directly from the - CD-ROM without installation to your hard drive. - -FD14-LegacyCD.zip - - * FD14BOOT.IMG - This zip archive also contains a copy of the basic - CD-ROM installation boot floppy. - - * FD14LGCY.ISO - A bootable CD image designed for older hardware. If - you cannot boot the LiveCD to install FreeDOS, try this disc image. - - This disc image uses the older El Torito boot CD format. Some newer - computers and virtual machines cannot use this older format. Unless - you have a computer that requires this type of bootable CD, we - recommend using the LiveCD instead. - -FD14-BonusCD.zip - - * FD14BNS.ISO - A non-bootable CD image that contains some FreeDOS - packages that are not installed as part of either the LiveCD or - the Legacy CD. - -FD14-LiteUSB.zip - - * FD14LITE.IMG - A minimal FreeDOS installer, as a USB fob drive - image. This does not contain all of the packages from either the - LiveCD or the LegacyCD, and instead only contains a basic set of - FreeDOS packages. - - * FD14LITE.VMDK - A virtual machine disk file, compatible with a - variety of virtual machine software including VirtualBox, VMware, - and other systems. - - Using a VMDK file can simplify installing FreeDOS. Just attach the - VMDK image to your virtual machine software as a hard drive, and - boot it. (Please note that you will still need to create a virtual - hard disk to install FreeDOS) - -FD14-FullUSB.zip - - * FD14FULL.IMG - Plain DOS system and Full install USB stick image. - - * FD14FULL.VMDK - A virtual machine disk file, compatible with a - variety of virtual machine software. Just attach the VMDK image to - your virtual machine as a hard drive, and boot it. - -VERIFY.TXT - - * Contains MD5, SHA256 and SHA512 hashes for all of the different - release files. You can verify your copy of FreeDOS with these. - -README.TXT - - * The "before you choose and install" document. (All of the zip - files listed above also have a copy of the README file.) - - -------------------------------------------------------------------------------- - FreeDOS Floppy-Only Edition (FD14-x86) -------------------------------------------------------------------------------- - -FreeDOS 1.4 includes a Floppy-Only Edition! This edition should run on -any hardware that can run FreeDOS and has EGA or better graphics: - - * Are you running a '286 or another classic system without a CD-ROM - drive? Install from these floppies to install FreeDOS. - - * Do you have just one hard drive and no CD or floppy drive? Just - copy the contents of the floppies to a temporary directory and run - the installer from there. - - * Want to perform a "headless" install to a different DOS directory? - It's easy with the command line options. - -The Floppy-Only Edition uses a completely different installer than -the CD-ROM or USB installers. The Floppy-Only Edition does not use -any of those other media to install. - -The Floppy-Only Edition contains a limited set of FreeDOS programs -that are more useful on classic PC hardware. - -The FreeDOS Floppy-Only Edition is distributed as single zip archive that -contains several pre-made floppy diskette images: - - These zip archives contain image files for several common floppy - diskette media under separate directories: - - * 720k - 3.5" 720k diskette images - - * 144m - 3.5" 1.44mb diskette images - - * 120m - 5.25" 1.2mb diskette images - - Each of those sets contain a number of pre-made disk images: - - * x86BOOT.img - A floppy boot disk image with the x86 installer. - - * x86DSK??.img - Several floppy diskette images that contain the - core FreeDOS operating system files. The number of floppy images - and amount of files on each varies depending on the diskette - capacity. - -To conserve space, the FreeDOS Floppy-Only Edition does not contain -the source code for the FreeDOS packages. You can find the source code -via the FreeDOS website (https://www.freedos.org/download/) or from -the other release media, like the USB or CD-ROM installer. - diff --git a/.qemu/send-keys.ps1 b/.qemu/send-keys.ps1 deleted file mode 100644 index 3431978..0000000 --- a/.qemu/send-keys.ps1 +++ /dev/null @@ -1,60 +0,0 @@ -param( - [Parameter(Mandatory)] - [string] $Text, - - [ValidateRange(0, 1000)] - [int] $DelayMilliseconds = 25, - - [switch] $NoEnter -) - -$ErrorActionPreference = 'Stop' - -$map = @{ - ' ' = 'spc'; ':' = 'shift-semicolon'; ';' = 'semicolon' - '\' = 'backslash'; '|' = 'shift-backslash'; '/' = 'slash'; '?' = 'shift-slash' - '.' = 'dot'; '>' = 'shift-dot'; ',' = 'comma'; '<' = 'shift-comma' - '-' = 'minus'; '_' = 'shift-minus'; '=' = 'equal'; '+' = 'shift-equal' - '[' = 'bracket_left'; '{' = 'shift-bracket_left' - ']' = 'bracket_right'; '}' = 'shift-bracket_right' - "'" = 'apostrophe'; '"' = 'shift-apostrophe'; '`' = 'grave_accent'; '~' = 'shift-grave_accent' - '!' = 'shift-1'; '@' = 'shift-2'; '#' = 'shift-3'; '$' = 'shift-4'; '%' = 'shift-5' - '^' = 'shift-6'; '&' = 'shift-7'; '*' = 'shift-8'; '(' = 'shift-9'; ')' = 'shift-0' -} - -function ConvertTo-QemuKey([char] $Character) { - $text = [string] $Character - if ($map.ContainsKey($text)) { return $map[$text] } - if ([char]::IsLetter($Character)) { - $letter = [char]::ToLowerInvariant($Character) - if ([char]::IsUpper($Character)) { return "shift-$letter" } - return [string] $letter - } - if ([char]::IsDigit($Character)) { return $text } - throw "Unsupported QEMU key character: '$Character'" -} - -$client = [System.Net.Sockets.TcpClient]::new([System.Net.Sockets.AddressFamily]::InterNetwork) -try { - $client.Connect([System.Net.IPAddress]::Parse('127.0.0.1'), 4444) - $stream = $client.GetStream() - $writer = [System.IO.StreamWriter]::new($stream, [System.Text.Encoding]::ASCII, 1024, $true) - try { - $writer.NewLine = "`r`n" - $writer.AutoFlush = $true - Start-Sleep -Milliseconds 100 - - foreach ($char in $Text.ToCharArray()) { - $writer.WriteLine("sendkey $(ConvertTo-QemuKey $char)") - Start-Sleep -Milliseconds $DelayMilliseconds - } - if (-not $NoEnter) { $writer.WriteLine('sendkey ret') } - Start-Sleep -Milliseconds $DelayMilliseconds - } - finally { - $writer.Dispose() - } -} -finally { - $client.Dispose() -} diff --git a/.qemu/setup.ps1 b/.qemu/setup.ps1 deleted file mode 100644 index 58b944e..0000000 --- a/.qemu/setup.ps1 +++ /dev/null @@ -1,26 +0,0 @@ -param() - -$ErrorActionPreference = 'Stop' -$qemuImg = Get-Command qemu-img.exe -ErrorAction Stop -$disk = Join-Path $PSScriptRoot 'freedos.qcow2' -$archive = Join-Path $PSScriptRoot 'FD14-LiveCD.zip' -$iso = Join-Path $PSScriptRoot 'FD14LIVE.iso' -$url = 'https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/distributions/1.4/FD14-LiveCD.zip' - -if (-not (Test-Path -LiteralPath $disk)) { - & $qemuImg.Source create -f qcow2 $disk 2G - if ($LASTEXITCODE -ne 0) { throw "qemu-img failed with exit code $LASTEXITCODE." } -} - -if (-not (Test-Path -LiteralPath $iso)) { - if (-not (Test-Path -LiteralPath $archive)) { - Invoke-WebRequest -Uri $url -OutFile $archive - } - Expand-Archive -LiteralPath $archive -DestinationPath $PSScriptRoot -Force -} - -if (-not (Test-Path -LiteralPath $iso)) { - throw 'FreeDOS ISO extraction failed.' -} - -Get-Item -LiteralPath $disk,$iso | Select-Object Name,Length,LastWriteTime diff --git a/AGENTS.md b/AGENTS.md index c9590c2..ad78332 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,53 +10,39 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 | `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | | `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | | `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | -| `tools/tcpagent/README.md` | DOS 내부 TCP 에이전트 프로토콜과 빌드 | -VM 자동화 **명령 목록과 플래그는 문서가 아니라 CLI가 규범**이다. 문서에 복제하면 -반드시 드리프트하므로 아래로 확인한다. +개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다. ```powershell -uv run ferro-vm --help -uv run ferro-vm --help +uv run ferro-dos --help +uv run ferro-dos --help uv run ferro-test --help ``` ## 검증 규칙 -- 개발 중 빠른 회귀 검사는 `uv run ferro-test run --through `로 - DOSBox-X에서 수행한다. - 이 경로도 컴파일러 A를 DOS 내부 Open Watcom으로 매번 새로 빌드한다. -- **마일스톤의 최종 공식 검증은 QEMU FreeDOS 내부에서만 한다.** DOSBox-X 결과는 - 개발용 smoke test이며 완료 게이트를 대체하지 않는다. -- 컴파일러 A와 생성 C 모두 VM 안의 Open Watcom으로 컴파일한다. - 컴파일러 A와 bits16은 `WCL`, bits32 생성 C는 `WCL386`. -- **호스트에서 컴파일하지 않는다.** WSL이나 호스트 C 컴파일러 결과는 정식 검증으로 - 인정하지 않는다. 호스트는 편집, diff, Git, 파일 전송에만 쓴다. -- authoritative workspace는 VM의 `C:\FEC`다. -- 마일스톤 완료 기준은 `C:\FEC\BUILD.OK`, `C:\FEC\TEST.OK`, `TEST-DOS.BAT` exit 0 - 세 가지를 모두 확인하는 것이다. 테스트가 증명하지 않는 기능은 완료로 처리하지 - 않는다. -- VGA 데모처럼 멀티모달 수동 검증이 필요한 항목은 완료 게이트에서 제외한다. +- 컴파일러와 생성 C는 DOSBox-X 내부의 고정된 Open Watcom으로 컴파일한다. + 컴파일러와 bits16은 `WCL`, bits32 생성 C는 `WCL386`을 쓴다. +- 호스트 C 컴파일러 결과는 검증으로 인정하지 않는다. 호스트는 편집, Git, 다운로드, + 격리 작업공간 준비에만 쓴다. +- 실행마다 만들어지는 authoritative workspace는 `C:\FEC`다. 호스트에서는 + `.dosboxx/runs//FEC`에 대응한다. +- 완료하려는 기능을 직접 검사하는 pytest case가 통과해야 한다. 테스트가 증명하지 + 않는 기능은 완료로 처리하지 않는다. +- VGA 데모처럼 수동 검증이 필요한 항목은 자동 완료 게이트에서 제외한다. ## 빌드 함정 -VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 실패가 난다. - -- **컴파일러 A는 16비트 large model로 빌드한다.** small model은 메모리 부족으로 - 실패한다. -- **링크는 `*.obj` 와일드카드로 한다.** DOS 명령줄 길이 제한 때문에 오브젝트를 - 나열할 수 없다. 그래서 `fec/build-dos.bat`는 먼저 `C:\FEC\*.obj`를 지워 - stale 32비트 test object가 섞이지 않게 한다. -- **M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다.** 생성 C의 보수적 미사용 helper 때문에 +- 컴파일러는 16비트 large model로 빌드한다. small model은 메모리 부족으로 실패한다. +- 링크는 `*.obj` 와일드카드로 한다. DOS 명령줄 길이 제한 때문에 오브젝트를 나열할 + 수 없다. `fec/build-dos.bat`는 먼저 stale object를 지운다. +- M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다. 생성 C의 보수적 미사용 helper 때문에 W202만 끄고 나머지 경고는 오류로 유지한다. -- **fixture는 짧은 이름으로 전송한다.** DOS 8.3 파일명 때문에 긴 이름은 - `BAD-ARI.FE`, `TRY-FPR.FE`처럼 명시적으로 줄여야 한다. -- **`D:`에서 빌드하지 않는다.** QEMU의 vvfat 뷰는 교환용이며, 과거 `D:`에서 빌드하다 - rename 처리 오류로 QEMU가 종료된 적이 있다. 호스트에서 편집한 파일은 `put`으로 - `C:`에 올린 뒤 컴파일한다. -- 긴 DOS 배치가 멈추면 QEMU를 재시작하기 전에 Ctrl+C 주입을 먼저 시도한다. - `Terminate batch file ... (Yes/No/All)?`가 뜨면 `y`, `ret`을 보내고 `ping` 복구를 - 확인한다. +- fixture는 DOS 8.3 이름으로 실행한다. 긴 이름은 registry에서 명시적으로 줄인다. +- `R:`은 읽기 전용 저장소, `W:`은 읽기 전용 Watcom이다. 빌드 산출물은 반드시 + 임시 `C:\FEC`에 쓴다. +- 실패 분석이 필요하면 `ferro-test --keep-failed` 또는 `ferro-dos --keep`으로 + 임시 작업공간을 보존한다. ## 작업 흐름 @@ -64,12 +50,9 @@ VM 안에서 반복해서 물렸던 것들. 어기면 원인 찾기 어려운 구현이 명세와 다르면 둘 중 하나가 틀린 것이므로 그 자리에서 결론을 낸다. - 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. - primary 브랜치는 `master`다. -- `.qemu/*.png`, `.qemu/*.ppm`은 진단용이며 커밋하지 않는다. +- `.dosboxx/`의 다운로드, 실행 작업공간, 로그는 커밋하지 않는다. ## 현재 상태 -- M1~M5 완료 및 QEMU/Open Watcom 검증됨. -- 다음은 M6(R1~R8 대여 검사)다. 착수할 때 소유권 로직을 `check.c`/`emit_c.c`에서 - `own.c/h`로 분리한다 (`SPEC.md` §11.3). -- v0.1.6에서 R8(파생 반환), R6(마지막 사용까지 대여), R10(전역 대여 금지)이 - 바뀌었다. 셋 다 own.c의 상태 기계를 건드리므로 분리 이후에 함께 구현한다. +현재 구현 상태와 다음 마일스톤은 `SPEC.md`와 테스트 registry를 기준으로 판단한다. +과거 VM 이미지나 호스트에 남은 바이너리를 근거로 완료 처리하지 않는다. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index d6fcdd1..0000000 --- a/TODO.md +++ /dev/null @@ -1,27 +0,0 @@ -# TODO - -## Done - -- [x] Add a non-reboot abort path for a hung DOS command: inject `Ctrl+C` through - QEMU's monitor and wait for the agent to recover. -- [x] Apply a configurable timeout to `exec` and invoke the non-reboot abort path - on timeout. -- [x] Expose `abort` for an immediate user-requested command interruption. - -The three above were first built for the COM1 serial agent, lost in the rewrite -to the resident TCP agent, and rebuilt on the QEMU monitor in `ferro-vm exec`. -The TCP version supervises with guest disk liveness (`info blockstats`) rather -than a fixed stopwatch, so a slow compile is no longer mistaken for a hang. - -## Open - -- [ ] Consider `BREAK=ON` in `C:\FDCONFIG.SYS`. Ctrl+C only takes effect at a DOS - break check, and with the FreeDOS default of `BREAK=OFF` a compute-bound - child whose output is redirected to a file may never reach one, so `abort` - cannot always stop it. `BREAK=ON` checks on every DOS call and makes the - abort reliable, at a small cost to every DOS call. Needs a VM reboot; back - up `FDCONFIG.SYS` first. -- [ ] `TCPAGENT.EXE` connects from a fixed source port (`LOCAL_PORT 2058`). After - the host end closes, a reconnect reuses the same 4-tuple and can flap until - the old state ages out. Observed as a ~10s connect/disconnect cycle after - the daemon is killed mid-connection. diff --git a/fec/vm-m1.bat b/fec/vm-m1.bat deleted file mode 100644 index e6e76a3..0000000 --- a/fec/vm-m1.bat +++ /dev/null @@ -1,201 +0,0 @@ -@echo off -rem D: is the read-only exchange volume. Stage everything before running DOS tools. -if not exist C:\FEC md C:\FEC -if not exist C:\FEC\SRC md C:\FEC\SRC -if not exist C:\FEC\STD md C:\FEC\STD -if not exist C:\FEC\TESTS md C:\FEC\TESTS -if not exist C:\FEC\TESTS\PASS md C:\FEC\TESTS\PASS -if not exist C:\FEC\TESTS\FAIL md C:\FEC\TESTS\FAIL -if not exist C:\FEC\TESTS\M2 md C:\FEC\TESTS\M2 -if not exist C:\FEC\TESTS\M3 md C:\FEC\TESTS\M3 -if not exist C:\FEC\TESTS\M4 md C:\FEC\TESTS\M4 -if not exist C:\FEC\TESTS\M5 md C:\FEC\TESTS\M5 -if exist C:\FEC\VM.FAIL del C:\FEC\VM.FAIL -if exist C:\FEC\STAGE.FAIL del C:\FEC\STAGE.FAIL - -copy D:\FEC\BUILD-~1.BAT C:\FEC\BUILD.BAT > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TEST-DOS.BAT C:\FEC\TEST-DOS.BAT > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\SRC\ARENA.C C:\FEC\SRC\ARENA.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\ARENA.H C:\FEC\SRC\ARENA.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DIAG.C C:\FEC\SRC\DIAG.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DIAG.H C:\FEC\SRC\DIAG.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\LEXER.C C:\FEC\SRC\LEXER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\LEXER.H C:\FEC\SRC\LEXER.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\AST.C C:\FEC\SRC\AST.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\AST.H C:\FEC\SRC\AST.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\PARSER.C C:\FEC\SRC\PARSER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\PARSER.H C:\FEC\SRC\PARSER.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\DRIVER.C C:\FEC\SRC\DRIVER.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\TYPES.C C:\FEC\SRC\TYPES.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\TYPES.H C:\FEC\SRC\TYPES.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\CHECK.C C:\FEC\SRC\CHECK.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\CHECK.H C:\FEC\SRC\CHECK.H > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\EMIT_C.C C:\FEC\SRC\EMIT_C.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\SRC\EMIT_C.H C:\FEC\SRC\EMIT_C.H > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\STD\CORE.FE C:\FEC\STD\CORE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\FMT.FE C:\FEC\STD\FMT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\IO.FE C:\FEC\STD\IO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\LIST.FE C:\FEC\STD\LIST.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\MAP.FE C:\FEC\STD\MAP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\MEM.FE C:\FEC\STD\MEM.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\STR.FE C:\FEC\STD\STR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\STD\SYS.FE C:\FEC\STD\SYS.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\PASS\BASIC.FE C:\FEC\TESTS\PASS\BASIC.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\LITERALS.FE C:\FEC\TESTS\PASS\LITERALS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\KEYWOR~1.FE C:\FEC\TESTS\PASS\KEYWOR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\PASS\V012-F~1.FE C:\FEC\TESTS\PASS\V012-F.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\MISSIN~1.FE C:\FEC\TESTS\FAIL\MISSIN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\UNCLOS~1.FE C:\FEC\TESTS\FAIL\UNCLOS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\FAIL\LOGICA~1.FE C:\FEC\TESTS\FAIL\LOGICA.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\HELLO.FE C:\FEC\TESTS\M2\HELLO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\SCOPES.FE C:\FEC\TESTS\M2\SCOPES.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-CO~1.FE C:\FEC\TESTS\M2\BAD-CO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-CAST.FE C:\FEC\TESTS\M2\BAD-CA.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-ASSI~1.FE C:\FEC\TESTS\M2\BAD-AS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-UN~1.FE C:\FEC\TESTS\M2\BAD-UN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-UN~2.FE C:\FEC\TESTS\M2\BAD-UI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-AR~1.FE C:\FEC\TESTS\M2\BAD-AR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-TY~1.FE C:\FEC\TESTS\M2\BAD-TY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-RE~1.FE C:\FEC\TESTS\M2\BAD-RE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\BAD-VOID.FE C:\FEC\TESTS\M2\BAD-VO.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M2\CAST-W~1.FE C:\FEC\TESTS\M2\CAST-W.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\M3\STRUCT.FE C:\FEC\TESTS\M3\STRUCT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ENUM.FE C:\FEC\TESTS\M3\ENUM.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ARRAY.FE C:\FEC\TESTS\M3\ARRAY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\STR.FE C:\FEC\TESTS\M3\STR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\FOR.FE C:\FEC\TESTS\M3\FOR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\NESTED.FE C:\FEC\TESTS\M3\NESTED.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\CHAR.FE C:\FEC\TESTS\M3\CHAR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\ARRAYCTX.FE C:\FEC\TESTS\M3\ARRAYCTX.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BOUNDS.FE C:\FEC\TESTS\M3\BOUNDS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADFLD.FE C:\FEC\TESTS\M3\BADFLD.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADMAT.FE C:\FEC\TESTS\M3\BADMAT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADARR.FE C:\FEC\TESTS\M3\BADARR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADCYCLE.FE C:\FEC\TESTS\M3\BADCYCLE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADSTR.FE C:\FEC\TESTS\M3\BADSTR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADCHAR.FE C:\FEC\TESTS\M3\BADCHAR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADFIELD.FE C:\FEC\TESTS\M3\BADFIELD.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M3\BADINDEX.FE C:\FEC\TESTS\M3\BADINDEX.FE > nul -if errorlevel 1 goto stage_fail - -copy D:\FEC\TESTS\M4\FORMAT.FE C:\FEC\TESTS\M4\FORMAT.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-ARI~1.FE C:\FEC\TESTS\M4\BAD-ARI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-VERB.FE C:\FEC\TESTS\M4\BAD-VERB.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-RUN~1.FE C:\FEC\TESTS\M4\BAD-RUN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-TYP~1.FE C:\FEC\TESTS\M4\BAD-TYP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-TRY.FE C:\FEC\TESTS\M4\BAD-TRY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\TRY-FPR~1.FE C:\FEC\TESTS\M4\TRY-FPR.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-WRI~1.FE C:\FEC\TESTS\M4\BAD-WRI.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\PROP.FE C:\FEC\TESTS\M4\PROP.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\PROPTEST.C C:\FEC\TESTS\M4\PROPTEST.C > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-MANY.FE C:\FEC\TESTS\M4\BAD-MANY.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-OPEN.FE C:\FEC\TESTS\M4\BAD-OPEN.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M4\BAD-CLS.FE C:\FEC\TESTS\M4\BAD-CLS.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\DEFER.FE C:\FEC\TESTS\M5\DEFER.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\OWNED.FE C:\FEC\TESTS\M5\OWNED.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\BAD-MOVE.FE C:\FEC\TESTS\M5\BAD-MOVE.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\BAD-DES~1.FE C:\FEC\TESTS\M5\BAD-DES.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\RUNTIME.FE C:\FEC\TESTS\M5\RUNTIME.FE > nul -if errorlevel 1 goto stage_fail -copy D:\FEC\TESTS\M5\RUNTIME.C C:\FEC\TESTS\M5\RUNTIME.C > nul -if errorlevel 1 goto stage_fail - -call C:\FEC\TEST-DOS.BAT -if exist C:\FEC\TEST.OK goto vm_success -echo FAIL>C:\FEC\VM.FAIL -verify other 2>nul -goto stage_done - -:vm_success -cd C:\FEC -goto stage_done - -:stage_fail -echo FAIL>C:\FEC\STAGE.FAIL -echo FAIL>C:\FEC\VM.FAIL -verify other 2>nul - -:stage_done diff --git a/pyproject.toml b/pyproject.toml index 9b0a084..635c397 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,16 +1,14 @@ [project] name = "ferrolang" version = "0.1.0" -description = "Ferro language compiler and QEMU development automation" +description = "Ferro language compiler and reproducible DOS development tools" requires-python = ">=3.12" dependencies = [ - "onnxruntime>=1.28.0", "pytest>=9.0.0", - "rapidocr>=3.9.2", ] [project.scripts] -ferro-vm = "ferrolang_vm.cli:main" +ferro-dos = "ferrolang_vm.dos_cli:main" ferro-test = "ferrolang_vm.test_cli:main" [build-system] diff --git a/src/ferrolang_vm/__init__.py b/src/ferrolang_vm/__init__.py index 039a606..a63f890 100644 --- a/src/ferrolang_vm/__init__.py +++ b/src/ferrolang_vm/__init__.py @@ -1 +1 @@ -"""Windows-only QEMU and FreeDOS TCP-agent automation.""" +"""Reproducible DOS development tools for the Ferro compiler.""" diff --git a/src/ferrolang_vm/cli.py b/src/ferrolang_vm/cli.py deleted file mode 100644 index ff40ddb..0000000 --- a/src/ferrolang_vm/cli.py +++ /dev/null @@ -1,195 +0,0 @@ -"""Command line client for the Windows-only ferro-vm daemon.""" -from __future__ import annotations - -import argparse -import json -import shutil -import subprocess -import sys -import time -from multiprocessing.connection import Client -from pathlib import Path - -from .daemon import PIPE, ROOT - - -def rpc(payload: dict[str, object], start_daemon: bool = False) -> object: - try: - conn = Client(PIPE, family="AF_PIPE") - except (FileNotFoundError, OSError): - if not start_daemon: - raise RuntimeError("ferro-vm daemon is not running; run `uv run ferro-vm start`") - flags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) | getattr(subprocess, "DETACHED_PROCESS", 0) - subprocess.Popen([sys.executable, "-m", "ferrolang_vm.daemon"], cwd=ROOT, creationflags=flags, close_fds=True) - deadline = time.monotonic() + 5 - while True: - try: - conn = Client(PIPE, family="AF_PIPE") - break - except (FileNotFoundError, OSError): - if time.monotonic() >= deadline: - raise RuntimeError("ferro-vm daemon did not create its control pipe") - time.sleep(.1) - with conn: - conn.send(payload) - response = conn.recv() - if not response["ok"]: - raise RuntimeError(response["error"]) - return response["result"] - - -def wait_ready(timeout: int) -> bool: - """Wait quietly; do not turn an expected boot gap into error-log spam.""" - deadline = time.monotonic() + timeout - while time.monotonic() < deadline: - try: - status = rpc({"op": "status"}) - if status["agent_connected"] and str(rpc({"op": "ping"})["response"]).startswith("OK 504F4E47"): - return True - except RuntimeError: - pass - time.sleep(.5) - return False - - -def follow_logs() -> int: - log_path = ROOT / ".qemu" / "ferro-vm.log" - lnav = shutil.which("lnav.exe") or shutil.which("lnav") - if lnav: - return subprocess.run([lnav, str(log_path)]).returncode - print("lnav was not found; following the log with PowerShell.", file=sys.stderr) - return subprocess.run([ - "powershell", "-NoProfile", "-Command", - f"Get-Content -LiteralPath '{log_path}' -Wait", - ]).returncode - - -EPILOG = r"""examples: - uv run ferro-vm start boot the VM and start the daemon - uv run ferro-vm wait-ready block until TCPAGENT answers PING - uv run ferro-vm exec 'dir C:\FEC' run a DOS command, print exit code and output - uv run ferro-vm put fec/src/check.c 'C:\FEC\SRC\CHECK.C' - uv run ferro-vm get 'C:\FEC\TEST.OK' .qemu/TEST.OK - uv run ferro-vm exec --idle-timeout 180 'C:\FEC\BUILD-DOS.BAT' - uv run ferro-vm abort Ctrl+C the command running right now - uv run ferro-vm logs follow the structured daemon log - -The authoritative workspace is C:\FEC inside the VM. Never build on D: (the vvfat -view is for exchange only). See AGENTS.md for verification rules and build traps. -""" - -SIMPLE_COMMANDS = { - "start": "Start the daemon and boot QEMU. Safe to run when already up.", - "stop": "Quit QEMU cleanly and stop the daemon.", - "status": "Print daemon, QEMU, and TCPAGENT connection state as JSON.", - "ping": "Send PING to TCPAGENT. Expects 'OK 504F4E47' (PONG).", - "screenshot": "Capture the VGA console to a PPM/PNG under .qemu/.", - "ocr": "Capture the console and print recognized text (RapidOCR).", - "logs": "Follow the append-only daemon log. Uses lnav when available.", - "abort": "Interrupt the DOS command currently running (Ctrl+C via QEMU).", -} - - -def main() -> int: - parser = argparse.ArgumentParser( - prog="ferro-vm", - description="Windows-only QEMU/FreeDOS automation for the Ferro compiler.", - epilog=EPILOG, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - commands = parser.add_subparsers(dest="op", required=True, metavar="COMMAND") - for name, blurb in SIMPLE_COMMANDS.items(): - commands.add_parser(name, help=blurb, description=blurb) - - wait_help = "Block until TCPAGENT is connected and answers PING." - wait = commands.add_parser("wait-ready", help=wait_help, description=wait_help) - wait.add_argument("--timeout", type=int, default=45, metavar="SECONDS", - help="give up after this many seconds (default: %(default)s)") - - reset_help = "Quit QEMU cleanly, reboot it, and wait for TCPAGENT." - reset = commands.add_parser("reset", help=reset_help, description=reset_help) - reset.add_argument("--timeout", type=int, default=45, metavar="SECONDS", - help="give up after this many seconds (default: %(default)s)") - - exec_help = "Run a DOS command inside the VM and print its exit code and output." - execute = commands.add_parser( - "exec", help=exec_help, - description=exec_help + " Quote the command so the host shell does not eat" - r" backslashes: exec 'wcl386 -q HELLO.C'." - " A slow command is not a failed one: the wait ends" - " when the guest stops touching its disk, not when a" - " stopwatch expires.") - execute.add_argument("command", metavar="DOS_COMMAND", - help=r"command line to hand to COMMAND.COM, e.g. 'dir C:\FEC'") - execute.add_argument("--idle-timeout", type=float, default=60, metavar="SECONDS", - help="interrupt once the guest has made no disk access for" - " this long (default: %(default)s)") - execute.add_argument("--hard-timeout", type=float, default=900, metavar="SECONDS", - help="interrupt after this much total time regardless of" - " activity; the backstop for a CPU-bound hang" - " (default: %(default)s)") - - put_help = "Copy a host file into the VM." - put = commands.add_parser("put", help=put_help, description=put_help) - put.add_argument("source", type=Path, metavar="HOST_PATH", - help="file on this machine") - put.add_argument("destination", metavar="DOS_PATH", - help=r"target inside the VM, e.g. 'C:\FEC\SRC\CHECK.C'." - " DOS uses 8.3 names, so long fixtures must be shortened" - " explicitly (BAD-ARI.FE, TRY-FPR.FE)") - - get_help = "Copy a file out of the VM onto the host." - get = commands.add_parser("get", help=get_help, description=get_help) - get.add_argument("source", metavar="DOS_PATH", - help=r"file inside the VM, e.g. 'C:\FEC\TEST.OK'") - get.add_argument("destination", type=Path, metavar="HOST_PATH", - help="target on this machine") - - args = parser.parse_args() - - try: - if args.op == "logs": - return follow_logs() - if args.op == "wait-ready": - if wait_ready(args.timeout): - print(json.dumps({"agent": "PONG"})) - return 0 - raise RuntimeError("TCPAGENT did not become ready") - if args.op == "ocr": - result = rpc({"op": "screenshot"}) - import logging - logging.disable(logging.INFO) - from rapidocr import RapidOCR - recognized = RapidOCR()(result["path"]) - print("\n".join(recognized.txts or ())) - return 0 - if args.op == "reset": - rpc({"op": "stop"}, start_daemon=True) - time.sleep(.5) - rpc({"op": "start"}, start_daemon=True) - time.sleep(2) - rpc({"op": "monitor", "command": "sendkey ret"}) - if wait_ready(args.timeout): - print(json.dumps({"reset": "complete", "agent": "PONG"})) - return 0 - raise RuntimeError("TCPAGENT did not become ready") - payload: dict[str, object] = {"op": args.op} - if args.op == "exec": - payload["command"] = args.command - payload["idle_timeout"] = args.idle_timeout - payload["hard_timeout"] = args.hard_timeout - if args.op == "put": - payload["source"] = str(args.source.resolve()) - payload["destination"] = args.destination - if args.op == "get": - payload["source"] = args.source - payload["destination"] = str(args.destination.resolve()) - print(json.dumps(rpc(payload, start_daemon=args.op == "start"), ensure_ascii=False, indent=2)) - return 0 - except RuntimeError as exc: - print(f"ferro-vm: {exc}", file=sys.stderr) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/ferrolang_vm/daemon.py b/src/ferrolang_vm/daemon.py deleted file mode 100644 index 20b3e9d..0000000 --- a/src/ferrolang_vm/daemon.py +++ /dev/null @@ -1,467 +0,0 @@ -"""Long-lived Windows host for the FreeDOS TCP agent. - -The only automation TCP listener is 127.0.0.1:5558, used exclusively by -TCPAGENT.EXE. Local commands use a Windows named pipe. -""" -from __future__ import annotations - -import json -import logging -import os -import re -import select -import shutil -import socket -import subprocess -import sys -import threading -import time -from datetime import datetime, timezone -from multiprocessing.connection import Listener -from pathlib import Path - -ROOT = Path(__file__).resolve().parents[2] -QEMU = ROOT / ".qemu" -PIPE = r"\\.\pipe\ferrolang-vm" -AGENT_ADDRESS = ("127.0.0.1", 5558) -MONITOR_ADDRESS = ("127.0.0.1", 4444) -LOG_PATH = QEMU / "ferro-vm.log" - -# Short commands answer promptly, so a plain socket timeout is the right guard. -REQUEST_TIMEOUT = 30 -# EXEC is different: TCPAGENT is frozen inside system() for the whole command -# and cannot answer, so silence proves nothing. Wake up often, decide with -# guest liveness instead of a stopwatch, and never discard the connection just -# because a compile is slow. -EXEC_POLL_SECONDS = 2 -DEFAULT_IDLE_TIMEOUT = 60 -DEFAULT_HARD_TIMEOUT = 900 -# Budget for collecting the result after Ctrl+C, before giving up on the stream. -INTERRUPT_GRACE_SECONDS = 15 - - -class ExecInterrupted(RuntimeError): - """The supervisor decided to stop the running DOS command.""" - - -def configure_logging() -> None: - QEMU.mkdir(exist_ok=True) - handler = logging.FileHandler(LOG_PATH, encoding="utf-8") - handler.setFormatter(logging.Formatter("%(asctime)s.%(msecs)03dZ %(levelname)-7s %(message)s", "%Y-%m-%dT%H:%M:%S")) - logging.basicConfig(level=logging.INFO, handlers=[handler]) - logging.Formatter.converter = time.gmtime - - -def log_event(level: int, event: str, **fields: object) -> None: - suffix = " ".join(f"{key}={json.dumps(value, ensure_ascii=False)}" for key, value in fields.items()) - logging.log(level, "%s%s", event, f" {suffix}" if suffix else "") - - -class Host: - def __init__(self) -> None: - self.agent: socket.socket | None = None - self.agent_lock = threading.Lock() - self.agent_ready = threading.Event() - self.qemu: subprocess.Popen[bytes] | None = None - # QEMU accepts one monitor connection at a time, and the EXEC - # supervisor polls it while other control requests run concurrently. - self.monitor_lock = threading.Lock() - self.abort_requested = threading.Event() - self.exec_active = threading.Event() - - @staticmethod - def bind_agent_listener() -> socket.socket: - """Bind 5558 exclusively so a second daemon fails loudly. - - SO_REUSEADDR means something different on Windows than on Unix: it lets - another process bind an already-bound port and quietly take over new - connections, so a duplicate daemon would be silently half-working - instead of refusing to start. SO_EXCLUSIVEADDRUSE is the Windows way to - say "only me". - """ - server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - exclusive = getattr(socket, "SO_EXCLUSIVEADDRUSE", None) - if exclusive is not None: - server.setsockopt(socket.SOL_SOCKET, exclusive, 1) - else: - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(AGENT_ADDRESS) - server.listen(1) - return server - - def accept_agents(self, server: socket.socket) -> None: - log_event(logging.INFO, "agent listener ready", address="127.0.0.1:5558") - while True: - sock, peer = server.accept() - sock.settimeout(REQUEST_TIMEOUT) - with self.agent_lock: - if self.agent is not None: - sock.close() - log_event(logging.WARNING, "agent rejected", peer=str(peer), reason="already connected") - continue - self.agent = sock - try: - banner = self._read_line(sock).decode("ascii", "replace") - if banner != "TCPAGENT READY": - raise ConnectionError(f"invalid TCPAGENT banner: {banner!r}") - self.agent_ready.set() - log_event(logging.INFO, "agent connected", peer=f"{peer[0]}:{peer[1]}") - # Request() owns protocol reads. Between requests, peek only - # for EOF so TCPAGENT can reconnect without being rejected. - while self.agent is sock: - if self.agent_lock.acquire(blocking=False): - try: - readable, _, _ = select.select([sock], [], [], .25) - if readable and not sock.recv(1, socket.MSG_PEEK): - break - finally: - self.agent_lock.release() - else: - time.sleep(.05) - except (ConnectionError, OSError) as exc: - # Reset can close a connection during its banner. This is a - # per-connection event, never a reason to kill the listener. - log_event(logging.WARNING, "agent handshake/connection failed", - peer=f"{peer[0]}:{peer[1]}", error=str(exc)) - finally: - with self.agent_lock: - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - sock.close() - log_event(logging.INFO, "agent disconnected", peer=f"{peer[0]}:{peer[1]}") - - @staticmethod - def _read_line(sock: socket.socket, supervise=None) -> bytes: - # Partial input is kept across timeouts, so supervise() may fire in the - # middle of a line without losing what has already arrived. - out = bytearray() - while not out.endswith(b"\n"): - try: - part = sock.recv(1) - except TimeoutError: - if supervise is None: - raise - supervise() - continue - if not part: - raise ConnectionError("TCP agent closed connection") - out.extend(part) - return bytes(out).rstrip(b"\r\n") - - @staticmethod - def _read_exactly(sock: socket.socket, count: int, supervise=None) -> bytes: - chunks: list[bytes] = [] - while count: - try: - chunk = sock.recv(min(65536, count)) - except TimeoutError: - if supervise is None: - raise - supervise() - continue - if not chunk: - raise ConnectionError("TCP agent closed connection") - chunks.append(chunk) - count -= len(chunk) - return b"".join(chunks) - - def guest_idle_seconds(self) -> float | None: - """Seconds since the guest last touched a disk, or None if unknown. - - QEMU keeps counting while TCPAGENT is frozen inside system(), so this - is the one progress signal available during a long DOS command. A - purely CPU-bound command looks idle here, which is what the hard - timeout is for. - """ - try: - text = self.monitor("info blockstats") - except OSError: - return None - idle: float | None = None - for line in text.splitlines(): - operations = re.search(r"rd_operations=(\d+)", line) - elapsed = re.search(r"idle_time_ns=(\d+)", line) - if not operations or not elapsed or int(operations.group(1)) == 0: - continue - seconds = int(elapsed.group(1)) / 1e9 - idle = seconds if idle is None else min(idle, seconds) - return idle - - def request(self, command: str, payload: bytes = b"") -> str: - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - started = time.monotonic() - try: - sock.sendall(command.encode("ascii") + b"\n" + payload) - response = self._read_line(sock).decode("ascii", "replace") - except OSError as exc: - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - raise RuntimeError(f"TCPAGENT request failed: {exc}") from exc - log_event(logging.INFO, "agent request", command=command.split(" ", 1)[0], response=response[:200], elapsed_ms=round((time.monotonic()-started)*1000)) - if response.startswith("ERR "): - raise RuntimeError(bytes.fromhex(response[4:]).decode("utf-8", "replace")) - return response - - def ping(self) -> dict[str, object]: - return {"response": self.request("PING")} - - def _read_exec_result(self, sock: socket.socket, supervise=None) -> tuple[int, int, bytes]: - header = self._read_line(sock, supervise).decode("ascii", "replace") - fields = header.split() - if len(fields) == 4 and fields[0] == "RESULT": - code, length, flags = int(fields[1]), int(fields[2]), int(fields[3]) - return code, flags, self._read_exactly(sock, length, supervise) - if fields and fields[0] == "OK": - # Compatibility with an installed pre-RESULT agent. - return int(fields[1]), 1, bytes.fromhex(fields[2]) if len(fields) > 2 else b"" - raise RuntimeError("malformed EXEC response: " + header) - - def exec(self, command: str, idle_timeout: float = DEFAULT_IDLE_TIMEOUT, - hard_timeout: float = DEFAULT_HARD_TIMEOUT) -> dict[str, object]: - log_event(logging.INFO, "exec start", command=command, - idle_timeout=idle_timeout, hard_timeout=hard_timeout) - started = time.monotonic() - self.abort_requested.clear() - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - self.exec_active.set() - reported = started - interrupt_at: float | None = None - interrupted = "" - - def supervise() -> None: - """Called every EXEC_POLL_SECONDS while the agent stays silent.""" - nonlocal reported, interrupt_at, interrupted - now = time.monotonic() - idle = self.guest_idle_seconds() - if now - reported >= 15: - reported = now - log_event(logging.INFO, "exec running", elapsed_s=round(now-started, 1), - guest_idle_s=None if idle is None else round(idle, 1)) - if interrupt_at is None: - if self.abort_requested.is_set(): - interrupted = "aborted by request" - elif now - started > hard_timeout: - interrupted = f"hard timeout after {hard_timeout:.0f}s" - elif idle is not None and idle > idle_timeout: - interrupted = f"guest idle {idle:.0f}s exceeds {idle_timeout:.0f}s" - if interrupted: - interrupt_at = now - log_event(logging.WARNING, "exec interrupting", reason=interrupted) - self.monitor("sendkey ctrl-c") - return - waited = now - interrupt_at - # DOS answers Ctrl+C with "Terminate batch file (Y/N/A)?" and - # waits there. The prompt only appears once COMMAND.COM reaches - # the next batch line, which can be many seconds into a slow - # command, so answer on every poll rather than once: a single - # early 'y' is swallowed by whatever is still running. Send only - # 'y' -- the prompt takes one keystroke, and a trailing Enter - # gets read as "keep going". - self.monitor("sendkey y") - # Even answered, Ctrl+C is a request. It lands only at a DOS - # break check, and a DOS/4GW child (wcc386, wmake) runs in - # protected mode where it may never reach one, so the command - # can still run to completion. Keep collecting its result rather - # than abandoning a stream that still owes us one -- give up - # only once the guest has gone quiet too. - if waited > INTERRUPT_GRACE_SECONDS and (idle is None or idle > 5): - raise ExecInterrupted(interrupted + "; command did not stop") - - try: - encoded = command.encode("ascii", "replace").hex().upper() - sock.settimeout(EXEC_POLL_SECONDS) - sock.sendall(f"EXEC {encoded}\n".encode("ascii")) - code, flags, raw = self._read_exec_result(sock, supervise) - except (OSError, ExecInterrupted) as exc: - # Only now is the stream beyond repair; drop it so the agent - # reconnects with a clean protocol state. - if self.agent is sock: - self.agent = None - self.agent_ready.clear() - sock.close() - raise RuntimeError(f"TCPAGENT EXEC failed: {exc}") from exc - finally: - self.exec_active.clear() - self.abort_requested.clear() - try: - sock.settimeout(REQUEST_TIMEOUT) - except OSError: - pass - output = raw.decode("cp437", "replace") - for line in output.splitlines(): - log_event(logging.INFO, "dos output", line=line) - log_event(logging.INFO, "exec finish", exit=code, bytes=len(raw), flags=flags, - interrupted=interrupted or None, - elapsed_ms=round((time.monotonic()-started)*1000)) - result = {"exit": code, "output": output, "bytes": len(raw), "flags": flags} - if interrupted: - result["interrupted"] = interrupted - return result - - def abort(self) -> dict[str, object]: - if not self.exec_active.is_set(): - return {"aborted": False, "reason": "no command is running"} - self.abort_requested.set() - log_event(logging.INFO, "abort requested") - return {"aborted": True} - - def put(self, source: str, destination: str) -> dict[str, object]: - data = Path(source).read_bytes() - encoded = destination.encode("ascii").hex().upper() - self.request(f"PUT {encoded} {len(data)}", data) - stat = self.request(f"HASH {encoded}") - log_event(logging.INFO, "put", path=destination, bytes=len(data), stat=stat) - return {"path": destination, "bytes": len(data), "stat": stat} - - def get(self, source: str, destination: str) -> dict[str, object]: - encoded = source.encode("ascii").hex().upper() - with self.agent_lock: - if self.agent is None: - raise RuntimeError("TCPAGENT is not connected") - sock = self.agent - sock.sendall(f"GET {encoded}\n".encode("ascii")) - header = self._read_line(sock).decode("ascii", "strict").split() - if len(header) != 2 or header[0] != "DATA": - raise RuntimeError("GET failed: " + " ".join(header)) - remaining = int(header[1]) - chunks: list[bytes] = [] - while remaining: - chunk = sock.recv(min(65536, remaining)) - if not chunk: - raise RuntimeError("TCPAGENT closed during GET") - chunks.append(chunk) - remaining -= len(chunk) - target = Path(destination) - target.parent.mkdir(parents=True, exist_ok=True) - data = b"".join(chunks) - target.write_bytes(data) - log_event(logging.INFO, "get", path=source, bytes=len(data), destination=str(target)) - return {"path": source, "destination": str(target), "bytes": len(data)} - - def monitor(self, command: str) -> str: - with self.monitor_lock, socket.create_connection(MONITOR_ADDRESS, timeout=3) as sock: - sock.settimeout(1) - time.sleep(.1) - try: - sock.recv(4096) - except TimeoutError: - pass - sock.sendall(command.encode("ascii") + b"\n") - time.sleep(.2) - chunks: list[bytes] = [] - while True: - try: - chunk = sock.recv(4096) - except TimeoutError: - break - if not chunk: - break - chunks.append(chunk) - return b"".join(chunks).decode("ascii", "replace").strip() - - def start(self) -> dict[str, object]: - if self.qemu is not None and self.qemu.poll() is None: - return {"started": False, "reason": "already running"} - try: - self.monitor("info status") - return {"started": False, "reason": "already running (external)"} - except OSError: - pass - executable = shutil.which("qemu-system-i386.exe") - disk = QEMU / "freedos.qcow2" - if not executable: - raise RuntimeError("qemu-system-i386.exe is not on PATH") - if not disk.exists(): - raise RuntimeError(f"missing {disk}; run .qemu/setup.ps1 and .qemu/install.ps1") - self.qemu = subprocess.Popen([executable, "-machine", "pc,accel=whpx,kernel-irqchip=off,usb=on", "-smp", "1", "-m", "64", "-drive", f"file={disk},format=qcow2,if=ide,index=0,media=disk", "-nic", "user,model=ne2k_isa", "-monitor", "tcp:127.0.0.1:4444,server=on,wait=off", "-boot", "order=c", "-display", "default"], cwd=QEMU) - log_event(logging.INFO, "qemu started", pid=self.qemu.pid) - return {"started": True, "pid": self.qemu.pid} - - def stop(self) -> dict[str, object]: - try: - self.monitor("quit") - log_event(logging.INFO, "qemu stop requested") - except OSError: - pass - return {"stopped": True} - - def screenshot(self) -> dict[str, object]: - ppm, png = QEMU / "qemu-screen.ppm", QEMU / "qemu-screen.png" - self.monitor("screendump " + str(ppm).replace("\\", "/")) - ffmpeg = shutil.which("ffmpeg.exe") - if not ppm.exists() or not ffmpeg: - raise RuntimeError("screenshot failed or ffmpeg.exe is not on PATH") - subprocess.run([ffmpeg, "-y", "-loglevel", "error", "-i", str(ppm), str(png)], check=True) - ppm.unlink(missing_ok=True) - log_event(logging.INFO, "screenshot", path=str(png)) - return {"path": str(png)} - - def dispatch(self, request: dict[str, object]) -> object: - op = request["op"] - if op == "status": - try: - self.monitor("info status") - running = True - except OSError: - running = False - return {"agent_connected": self.agent_ready.is_set(), "qemu_running": running, "log": str(LOG_PATH)} - if op == "start": return self.start() - if op == "stop": return self.stop() - if op == "ping": return self.ping() - if op == "abort": return self.abort() - if op == "exec": - return self.exec(str(request["command"]), - float(request.get("idle_timeout", DEFAULT_IDLE_TIMEOUT)), - float(request.get("hard_timeout", DEFAULT_HARD_TIMEOUT))) - if op == "put": return self.put(str(request["source"]), str(request["destination"])) - if op == "get": return self.get(str(request["source"]), str(request["destination"])) - if op == "screenshot": return self.screenshot() - if op == "monitor": return {"output": self.monitor(str(request["command"]))} - raise ValueError(f"unknown operation: {op}") - - -def serve_pipe(host: Host) -> None: - listener = Listener(PIPE, family="AF_PIPE") - log_event(logging.INFO, "control pipe ready", pipe=PIPE) - - def handle(conn) -> None: - try: - request = conn.recv() - try: - conn.send({"ok": True, "result": host.dispatch(request)}) - except Exception as exc: - log_event(logging.ERROR, "control failed", error=str(exc)) - conn.send({"ok": False, "error": str(exc)}) - finally: - conn.close() - - while True: - # One thread per request: `abort` has to be answerable while a long - # `exec` is still holding the agent. - threading.Thread(target=handle, args=(listener.accept(),), daemon=True).start() - - -def main() -> None: - if os.name != "nt": - raise SystemExit("ferro-vm currently supports Windows only") - configure_logging() - host = Host() - try: - server = host.bind_agent_listener() - except OSError as exc: - log_event(logging.ERROR, "agent listener bind failed", address="127.0.0.1:5558", error=str(exc)) - raise SystemExit(f"another ferro-vm daemon already owns 127.0.0.1:5558 ({exc})") - threading.Thread(target=host.accept_agents, args=(server,), daemon=True).start() - serve_pipe(host) - - -if __name__ == "__main__": - main() diff --git a/src/ferrolang_vm/dos_cli.py b/src/ferrolang_vm/dos_cli.py new file mode 100644 index 0000000..02a0567 --- /dev/null +++ b/src/ferrolang_vm/dos_cli.py @@ -0,0 +1,80 @@ +"""General-purpose disposable DOSBox-X/Open Watcom environment CLI.""" +from __future__ import annotations + +import argparse +import subprocess +import sys +from pathlib import Path + +from .dosboxx import DosboxError, run_suite, setup +from .paths import ROOT +from .suite import Case + + +def _run(command: str | None, *, keep: bool, show_dos: bool) -> int: + cases = [] if command is None else [Case("command", 0, command, True)] + run = run_suite(cases, keep=keep, show_dos=show_dos, trace_dos=show_dos) + try: + if run.result() != "PASS": + print(run.log(), file=sys.stderr) + return 1 + if cases and run.result(cases[0]) != "PASS": + print(run.log(cases[0]), file=sys.stderr) + return 1 + if cases: + output = run.log(cases[0]) + if output: + print(output, end="" if output.endswith("\n") else "\n") + if keep: + print(f"DOS workspace: {run.root}") + return 0 + finally: + run.cleanup() + + +def main() -> int: + parser = argparse.ArgumentParser( + prog="ferro-dos", + description="Disposable directory-backed DOSBox-X/Open Watcom environment.", + ) + commands = parser.add_subparsers(dest="action", required=True) + prepare = commands.add_parser("setup", help="install the pinned DOSBox-X and Open Watcom tools") + prepare.add_argument("--accept-watcom-license", action="store_true") + for name, help_text in ( + ("build", "build the current FEC source inside DOS"), + ("exec", "build FEC and execute one DOS command"), + ("batch", "build FEC and call a repository DOS batch"), + ("shell", "build FEC and open an interactive DOS shell"), + ): + command = commands.add_parser(name, help=help_text) + command.add_argument("--keep", action="store_true", help="preserve the temporary DOS workspace") + command.add_argument("--show-dos", action="store_true", help="show and pause the DOS window") + if name == "exec": + command.add_argument("dos_command") + elif name == "batch": + command.add_argument("path", type=Path) + args = parser.parse_args() + try: + if args.action == "setup": + dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) + print(f"DOSBox-X: {dosbox}") + print(f"Open Watcom: {watcom}") + return 0 + if args.action == "build": + return _run(None, keep=args.keep, show_dos=args.show_dos) + if args.action == "exec": + return _run(args.dos_command, keep=args.keep, show_dos=args.show_dos) + if args.action == "shell": + return _run("COMMAND.COM", keep=args.keep, show_dos=True) + path = (ROOT / args.path).resolve() + if (path != ROOT and ROOT not in path.parents) or not path.is_file(): + raise DosboxError("batch path must be an existing file inside the repository") + relative = path.relative_to(ROOT).as_posix().replace("/", "\\").upper() + return _run(f"CALL R:\\{relative}", keep=args.keep, show_dos=args.show_dos) + except (DosboxError, subprocess.SubprocessError) as exc: + print(f"ferro-dos: {exc}", file=sys.stderr) + return 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index 581f14c..58b5ca8 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -12,7 +12,7 @@ import zipfile from dataclasses import dataclass from pathlib import Path -from .daemon import ROOT +from .paths import ROOT from .suite import Case @@ -96,28 +96,25 @@ def setup(*, accept_watcom_license: bool = False) -> tuple[Path, Path]: raise DosboxError("the DOSBox-X development backend currently supports Windows only") lock = _lock() dosbox, watcom, watcom_required = _required_paths(lock) - marker = CACHE / "SETUP.OK" - lock_hash = _sha256(LOCK_PATH) - if (marker.is_file() and marker.read_text(encoding="ascii").strip() == lock_hash - and dosbox.is_file() and all(path.is_file() for path in watcom_required)): - return dosbox, watcom - if not accept_watcom_license: + tools_ready = dosbox.is_file() and all(path.is_file() for path in watcom_required) + if not tools_ready and not accept_watcom_license: raise DosboxError( "Open Watcom is distributed under the Sybase Open Watcom Public License. " "Review tools/toolchains/dosboxx.lock.json and rerun " - "`uv run ferro-test setup --accept-watcom-license`." + "`uv run ferro-dos setup --accept-watcom-license`." ) - dosbox_spec = lock["dosboxx"] - watcom_spec = lock["open_watcom"] - assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) - _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") - _safe_extract(_download("Open Watcom", watcom_spec), watcom) + if not tools_ready: + dosbox_spec = lock["dosboxx"] + watcom_spec = lock["open_watcom"] + assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) + _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") + _safe_extract(_download("Open Watcom", watcom_spec), watcom) dosbox, watcom, watcom_required = _required_paths(lock) missing = [str(path.relative_to(CACHE)) for path in [dosbox, *watcom_required] if not path.is_file()] if missing: raise DosboxError("toolchain archive is missing: " + ", ".join(missing)) - marker.write_text(lock_hash + "\n", encoding="ascii") + (CACHE / "SETUP.OK").write_text(_sha256(LOCK_PATH) + "\n", encoding="ascii") return dosbox, watcom @@ -125,7 +122,7 @@ def resolve_tools() -> tuple[Path, Path]: lock = _lock() dosbox, watcom, required = _required_paths(lock) if not dosbox.is_file() or not all(path.is_file() for path in required): - raise DosboxError("toolchain is not installed; run `uv run ferro-test setup`") + raise DosboxError("toolchain is not installed; run `uv run ferro-dos setup`") return dosbox, watcom @@ -165,7 +162,8 @@ def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: ]) lines.extend([ "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", - "echo DONE>RUN.OK", *(["pause"] if show_dos else []), "exit", "", + "echo DONE>RUN.OK", + *(["pause"] if show_dos else []), "exit", "", ]) return "\r\n".join(lines) @@ -236,7 +234,8 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, command.append("-silent") command.extend([ "-fastlaunch", "-conf", str(config), - "-c", f'mount C "{run_root}"', "-c", f'mount W "{watcom}" -ro', + "-c", f'mount C "{run_root}"', "-c", f'mount R "{ROOT}" -ro', + "-c", f'mount W "{watcom}" -ro', "-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT", ]) completed = subprocess.run(command, check=False, timeout=300) diff --git a/src/ferrolang_vm/paths.py b/src/ferrolang_vm/paths.py new file mode 100644 index 0000000..87ffbbd --- /dev/null +++ b/src/ferrolang_vm/paths.py @@ -0,0 +1,5 @@ +"""Repository and local cache paths shared by Ferro developer tools.""" +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] diff --git a/tools/README.md b/tools/README.md index 2e3429e..97b141e 100644 --- a/tools/README.md +++ b/tools/README.md @@ -2,75 +2,54 @@ ## Host support -Automation currently supports **Windows 10/11 only**. The fast development loop -requires only `uv`; it downloads pinned DOSBox-X and Open Watcom DOS releases. -The final milestone gate additionally requires QEMU with WHPX support and -`ffmpeg.exe` on `PATH`. Other hosts are not supported yet. - -## Getting started +The automated DOS development environment currently supports Windows 10/11. +The host only needs `uv`. The setup command downloads the pinned DOSBox-X and +Open Watcom DOS archives, verifies their SHA-256 hashes, and installs them in the +ignored `.dosboxx/` cache. + +```powershell +uv run ferro-dos setup --accept-watcom-license +``` + +Review the Open Watcom license referenced by +`tools/toolchains/dosboxx.lock.json` before accepting it. Neither downloaded +archives nor installed tools are committed. + +## General DOS environment + +`ferro-dos` provides the development entry points: + +```powershell +uv run ferro-dos build +uv run ferro-dos exec "FEC.EXE --check TESTS\M6\OKLAST.FE" +uv run ferro-dos batch fec\test-dos.bat +uv run ferro-dos shell +uv run ferro-dos --help +``` + +Every invocation creates an isolated host directory under `.dosboxx/runs/` and +mounts it as writable `C:`. The repository is mounted read-only as `R:` and the +pinned Open Watcom installation as read-only `W:`. Current compiler sources, +the standard library, and fixtures are copied to `C:\FEC`; all compilation and +execution happen there inside DOSBox-X. Successful runs are removed by default. +Use `--keep` to retain a workspace and `--show-dos` to display the DOS window. + +This directory-backed layout deliberately has no QEMU, disk-image, TCP-agent, +or OCR dependency. A future disk-image backend can be added without changing +the command interface. + +## Pytest regression suite + +`ferro-test` uses the same isolated DOSBox-X/Open Watcom environment, builds +`FEC.EXE` once, and executes all selected cases sequentially in that one DOS +instance. Pytest still reports each registered case separately. ```powershell -uv run ferro-test setup --accept-watcom-license uv run ferro-test run --through m6 -v -``` - -`setup` reads `tools/toolchains/dosboxx.lock.json`, downloads the exact official -archives, verifies their SHA-256 hashes, and extracts them under ignored -`.dosboxx/`. Review the Open Watcom license referenced by the lock file before -accepting it. Archives and installed tools are deliberately not committed. - -Each `run` creates a disposable DOS drive, copies the current compiler, standard -library, and fixtures, then builds `FEC.EXE` once inside DOS with Open Watcom. -All selected milestone commands execute sequentially in that same DOSBox-X -instance, while pytest reports every emit, Watcom build, runtime, and rejection -check separately. Thus stale QEMU binaries cannot make the test pass. - -`--through m6` runs cumulatively from M1; `--only m6` selects one milestone. -Use `--keep-failed` to preserve a failed drive under `.dosboxx/runs/`, -`--dos-log` to print the captured DOS console, `--trace-dos` to disable command -output redirection, and `--show-dos` to keep the GUI open until a key is pressed. - -This is the quick development smoke test. Run the QEMU/FreeDOS workflow below -for the authoritative milestone completion gate. - -```powershell -uv run ferro-vm start -uv run ferro-vm status -``` - -The command list lives in the CLI itself, not in this file: - -```powershell -uv run ferro-vm --help -uv run ferro-vm --help +uv run ferro-test run --only m6 --dos-log uv run ferro-test --help ``` -Working rules, verification gates, and DOS build traps are in `AGENTS.md`. - -## How it fits together - -`TCPAGENT.EXE` runs inside FreeDOS and dials out to `127.0.0.1:5558`; its wire -protocol is documented in `tcpagent/README.md`. The `ferro-vm` daemon owns that -connection and the QEMU monitor. Local commands reach the daemon over the -Windows named pipe `\\.\pipe\ferrolang-vm` — there is no controller or observer -TCP port. - -The daemon writes an append-only structured log (`uv run ferro-vm logs`, which -uses `lnav` when installed and otherwise falls back to PowerShell `Get-Content --Wait`). It records command metadata, DOS output, exit status, transfers, and -agent lifecycle events as UTF-8 lines, and deliberately never logs raw binary -payloads or protocol hex. - -`reset` quits QEMU cleanly, restarts it, waits for FreeDOS to boot, submits the -default boot-menu Enter, and requires a TCPAGENT `PING`/`PONG` before returning. -QEMU `system_reset` is intentionally unsupported: repeated soft resets leave the -FreeDOS NE2000 packet driver stuck during initialization. - -## Standalone OCR - -`tools/qemu_ocr.py` remains available for OCRing an existing image: - -```powershell -uv run python tools/qemu_ocr.py --image .qemu/qemu-screen.png -``` +`--keep-failed` preserves a failed workspace, `--dos-log` prints captured DOS +output, `--trace-dos` disables per-command redirection, and `--show-dos` displays +the GUI. Working rules and DOS/Open Watcom build traps are in `AGENTS.md`. diff --git a/tools/qemu_ocr.py b/tools/qemu_ocr.py deleted file mode 100644 index 949380f..0000000 --- a/tools/qemu_ocr.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 -"""Capture the QEMU VGA console and print its text with RapidOCR.""" - -from __future__ import annotations - -import argparse -import json -import logging -import subprocess -import sys -from pathlib import Path - -from rapidocr import RapidOCR - -ROOT = Path(__file__).resolve().parent.parent -DEFAULT_IMAGE = ROOT / ".qemu" / "qemu-screen.png" - - -def capture() -> Path: - subprocess.run( - ["ferro-vm", "screenshot"], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE, - text=True, - ) - if not DEFAULT_IMAGE.is_file(): - raise RuntimeError(f"QEMU screenshot was not created: {DEFAULT_IMAGE}") - return DEFAULT_IMAGE - - -def recognize(image: Path, min_score: float) -> list[dict[str, object]]: - logging.disable(logging.INFO) - result = RapidOCR()(image) - rows: list[dict[str, object]] = [] - if result.txts is None or result.boxes is None or result.scores is None: - return rows - for box, text, score in zip(result.boxes, result.txts, result.scores): - if float(score) < min_score: - continue - rows.append( - { - "x": int(min(point[0] for point in box)), - "y": int(min(point[1] for point in box)), - "text": text, - "score": round(float(score), 5), - } - ) - rows.sort(key=lambda row: (int(row["y"]), int(row["x"]))) - return rows - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--image", type=Path, help="OCR an existing image instead of capturing QEMU") - parser.add_argument("--min-score", type=float, default=0.5) - parser.add_argument("--json", action="store_true", help="emit locations and scores as JSON") - parser.add_argument("-o", "--output", type=Path, help="also save output as UTF-8 text") - args = parser.parse_args() - - image = args.image.resolve() if args.image else capture() - rows = recognize(image, args.min_score) - if args.json: - rendered = json.dumps({"image": str(image), "lines": rows}, ensure_ascii=False, indent=2) - else: - rendered = "\n".join(str(row["text"]) for row in rows) - if args.output: - args.output.write_text(rendered + ("\n" if rendered else ""), encoding="utf-8") - if rendered: - print(rendered) - return 0 if rows else 1 - - -if __name__ == "__main__": - try: - raise SystemExit(main()) - except (OSError, RuntimeError, subprocess.CalledProcessError) as exc: - print(f"qemu-ocr: {exc}", file=sys.stderr) - raise SystemExit(2) diff --git a/tools/tcpagent/BUILD.BAT b/tools/tcpagent/BUILD.BAT deleted file mode 100644 index fa13ec9..0000000 --- a/tools/tcpagent/BUILD.BAT +++ /dev/null @@ -1,6 +0,0 @@ -@echo off -set WATCOM=C:\DEVEL\WATCOMC -set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN -set INCLUDE=C:\DEVEL\WATCOMC\H -set EDPATH= -wmake diff --git a/tools/tcpagent/INSTALL.BAT b/tools/tcpagent/INSTALL.BAT deleted file mode 100644 index 5532775..0000000 --- a/tools/tcpagent/INSTALL.BAT +++ /dev/null @@ -1,15 +0,0 @@ -@echo off -if not exist TCPAGENT.EXE goto fail -copy TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE > nul -if errorlevel 1 goto fail -find "TCPAGENT.EXE" C:\FDAUTO.BAT > nul -if not errorlevel 1 goto done -echo SET MTCPCFG=C:\FREEDOS\MTCP.CFG>>C:\FDAUTO.BAT -echo IF EXIST C:\FREEDOS\BIN\TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE>>C:\FDAUTO.BAT -:done -echo TCPAGENT installed. -goto end -:fail -echo TCPAGENT install failed. -verify other 2>nul -:end diff --git a/tools/tcpagent/Makefile b/tools/tcpagent/Makefile deleted file mode 100644 index ceb4f0f..0000000 --- a/tools/tcpagent/Makefile +++ /dev/null @@ -1,38 +0,0 @@ -# Place this directory at MTCP\APPS\TCPAGENT in an mTCP checkout. -tcp_c_dir = ..\..\TCPLIB -memory_model = -mc -compile_options = @WPP.RSP -tcpobjs = packet.obj arp.obj eth.obj ip.obj tcp.obj tcpsockm.obj udp.obj utils.obj timer.obj ipasm.obj trace.obj -all : tcpagent.exe -.cpp.obj : - wpp $[* $(compile_options) -tcpagent.obj: tcpagent.cpp - wpp $(compile_options) tcpagent.cpp -packet.obj: - wpp $(tcp_c_dir)\packet.cpp $(compile_options) -arp.obj: - wpp $(tcp_c_dir)\arp.cpp $(compile_options) -eth.obj: - wpp $(tcp_c_dir)\eth.cpp $(compile_options) -ip.obj: - wpp $(tcp_c_dir)\ip.cpp $(compile_options) -tcp.obj: - wpp $(tcp_c_dir)\tcp.cpp $(compile_options) -tcpsockm.obj: - wpp $(tcp_c_dir)\tcpsockm.cpp $(compile_options) -udp.obj: - wpp $(tcp_c_dir)\udp.cpp $(compile_options) -utils.obj: - wpp $(tcp_c_dir)\utils.cpp $(compile_options) -timer.obj: - wpp $(tcp_c_dir)\timer.cpp $(compile_options) -trace.obj: - wpp $(tcp_c_dir)\trace.cpp $(compile_options) -ipasm.obj: - wasm -0 -mc $(tcp_c_dir)\ipasm.asm -tcpagent.exe: tcpagent.obj $(tcpobjs) - wlink system dos option map option eliminate option stack=16384 name $@ file *.obj -clean : .symbolic - del *.obj - del *.map - del tcpagent.exe diff --git a/tools/tcpagent/README.md b/tools/tcpagent/README.md deleted file mode 100644 index 249b8c8..0000000 --- a/tools/tcpagent/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# FreeDOS resident TCP agent - -`TCPAGENT.EXE` is a foreground resident automation process. It uses the mTCP -packet-driver stack and maintains an outbound connection to the QEMU host at -`10.0.2.2:5558`. - -The Windows-only Python `ferro-vm` daemon owns that listener. It logs metadata -and decoded command output to `.qemu/ferro-vm.log`; it does not expose an -observer/controller TCP port or emit binary payloads to the log. Local host -control uses a Windows named pipe. - -## Build in FreeDOS - -1. Obtain the GPLv3 mTCP source tree (tested with the jhpyle/mTCP 2022 fork). -2. Copy this directory to `MTCP\APPS\TCPAGENT` inside that tree. -3. Set `WATCOM` for Open Watcom and run `BUILD.BAT`. -4. Run `INSTALL.BAT`; it installs the executable and adds startup lines after - the existing packet-driver setup in `C:\FDAUTO.BAT`. - -The build uses mTCP's compact memory model and Open Watcom C++16. The agent is -therefore distributed under GPLv3 when linked with mTCP. - -## Protocol - -`PING`, `READ`, `WRITE`, and `LIST` use text commands. `EXEC` captures both -stdout and stderr at the DOS handle level and returns an untruncated raw body: - -- `EXEC \n` -> `RESULT \r\n` - -Fast transfer commands are: - -- `PUT \n` -> `OK\r\n` -- `GET \n` -> `DATA \r\n` -- `HASH \n` -> `STAT \r\n` - -The host invokes them through: - -```powershell -uv run ferro-vm put host-file 'C:\DOS\FILE' -uv run ferro-vm get 'C:\DOS\FILE' host-file -``` - -## Agent-side logging - -The foreground agent prints one timestamped line per event on the VGA console -and keeps the same text in `C:\TCPAGENT.LOG`, rotating files larger than 256 KiB -to `C:\TCPAGENT.OLD`. Payloads and command output are never written to that -metadata log. - -Every command is logged with a request line and a result line carrying byte -counts and elapsed time — `EXEC`, `PUT`, `GET`, `HASH`, `LIST`, `READ`, and -`WRITE`. `PING` is deliberately excluded because `wait-ready` polls it twice a -second. Connection events (`connecting`, `connected`, `connect failed; retry N`, -`link lost`) are logged too; those are invisible to the host by definition, -since they happen when the socket is down. - -Lines are colored by writing VGA attribute bytes after `cprintf` lays out the -line: gray timestamps, cyan requests, yellow `EXEC` command text, green success, -red failure. Open Watcom's DOS `conio.h` has no `textattr()`, and ANSI escapes -are not interpreted on this FreeDOS console, so neither of the usual routes -works. Elapsed times come from the BIOS tick counter at 18.2065 Hz (~55 ms -resolution). - -## Long commands - -mTCP is only driven when the agent calls it, and `system()` freezes the agent -for the entire child command. So during a long `EXEC` the DOS side is mute: it -cannot answer, cannot acknowledge, cannot report progress. Silence therefore -proves nothing about whether the command is healthy. - -The host must not read that silence as failure. `ferro-vm exec` waits on -QEMU's own view of the guest instead: `info blockstats` keeps counting while -the agent is frozen, and `idle_time_ns` distinguishes a slow command from a -stuck one. See `--idle-timeout` and `--hard-timeout` in `ferro-vm exec --help`. - -When the host does decide to stop a command it injects Ctrl+C through the QEMU -monitor, then answers COMMAND.COM's `Terminate batch file (Y/N/A)?` prompt. -That is a request, not a guarantee: Ctrl+C only lands at a DOS break check, and -with `BREAK=OFF` (the FreeDOS default in `C:\FDCONFIG.SYS`) a compute-bound -child whose output we redirected to a file may never reach one. The host keeps -collecting the result either way rather than abandoning a stream that still -owes it a `RESULT`. - -Adding `BREAK=ON` to `C:\FDCONFIG.SYS` would make DOS check on every system -call and so make Ctrl+C reliable, at a small cost to every DOS call. - -## Rebuilding inside the VM - -`REBUILD.BAT` compiles and installs the agent in a single `exec`. `BUILD.BAT` -only runs `wmake` in the current directory, which is not where a host-driven -`ferro-vm exec` starts. - -```powershell -uv run ferro-vm put tools/tcpagent/tcpagent.cpp 'C:\MTSRC\MTCP\APPS\TCPAGENT\TCPAGENT.CPP' -uv run ferro-vm put tools/tcpagent/REBUILD.BAT 'C:\REBUILD.BAT' -uv run ferro-vm exec 'C:\REBUILD.BAT' -uv run ferro-vm reset -``` - -The reset is required: the running agent holds the old image in memory, and -`C:\FDAUTO.BAT` starts it at boot. diff --git a/tools/tcpagent/REBUILD.BAT b/tools/tcpagent/REBUILD.BAT deleted file mode 100644 index 989d172..0000000 --- a/tools/tcpagent/REBUILD.BAT +++ /dev/null @@ -1,22 +0,0 @@ -@echo off -rem Rebuild TCPAGENT.EXE from C:\MTSRC and install it, in one EXEC. -rem BUILD.BAT only runs wmake in the current directory, which is not where a -rem host-driven `ferro-vm exec` starts. Copy this to C:\ and run it by path. -rem The running agent keeps the old image in memory, so reset the VM afterwards: -rem uv run ferro-vm reset -C: -cd C:\MTSRC\MTCP\APPS\TCPAGENT -set WATCOM=C:\DEVEL\WATCOMC -set PATH=C:\DEVEL\WATCOMC\BINW;C:\DEVEL\WATCOMC\BINP;C:\FREEDOS\BIN -set INCLUDE=C:\DEVEL\WATCOMC\H -set EDPATH= -if exist TCPAGENT.OBJ del TCPAGENT.OBJ -if exist TCPAGENT.EXE del TCPAGENT.EXE -wmake -if not exist TCPAGENT.EXE goto fail -copy /Y TCPAGENT.EXE C:\FREEDOS\BIN\TCPAGENT.EXE -echo BUILD-OK -goto end -:fail -echo BUILD-FAILED -:end diff --git a/tools/tcpagent/WPP.RSP b/tools/tcpagent/WPP.RSP deleted file mode 100644 index 08000f4..0000000 --- a/tools/tcpagent/WPP.RSP +++ /dev/null @@ -1,11 +0,0 @@ --0 --mc --DCFG_H="tcpagent.cfg" --oh --os --s --zp2 --zpw --we --i=..\..\TCPINC --i=..\..\INCLUDE diff --git a/tools/tcpagent/tcpagent.cfg b/tools/tcpagent/tcpagent.cfg deleted file mode 100644 index b962291..0000000 --- a/tools/tcpagent/tcpagent.cfg +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef CONFIG_H -#define CONFIG_H -#define MTCP_PROGRAM_NAME "tcpagent" -#include "Global.Cfg" -#define COMPILE_ARP -#define IP_FRAGMENTS_ON -#define COMPILE_UDP -#define COMPILE_TCP -#define COMPILE_ICMP -#undef TCP_MAX_SOCKETS -#define TCP_MAX_SOCKETS (1) -#endif diff --git a/tools/tcpagent/tcpagent.cpp b/tools/tcpagent/tcpagent.cpp deleted file mode 100644 index 574316d..0000000 --- a/tools/tcpagent/tcpagent.cpp +++ /dev/null @@ -1,303 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "types.h" -#include "trace.h" -#include "utils.h" -#include "packet.h" -#include "arp.h" -#include "udp.h" -#include "tcp.h" -#include "tcpsockm.h" - -#define LINE_SIZE 9000 -#define CHUNK_SIZE 4096 -#define SERVER_PORT 5558 -#define LOCAL_PORT 2058 -#define RECV_SIZE 12288 - -static char linebuf[LINE_SIZE]; -static unsigned char data[CHUNK_SIZE+1]; -static TcpSocket *socketp; -static volatile uint8_t stop_requested; -static FILE *put_file; -static unsigned long put_remaining; -static unsigned long put_started; -static char put_path[260]; - -static unsigned long ticks(void) { - long value=0; - _bios_timeofday(_TIME_GETCLOCK,&value); - return (unsigned long)value; -} -/* The BIOS tick is 18.2065 Hz, so one tick is 5.49254 hundredths of a second. - 549/100 keeps the error under 0.05% and cannot overflow 32 bits for a delta - up to a full day (1573040 ticks * 549 fits). Plain delta/18 ran 1.1% fast. */ -static void elapsed_text(unsigned long started,char *out) { - unsigned long now=ticks(),delta=now>=started?now-started:now+(1573040UL-started); - unsigned long hundredths=delta*549UL/100UL; - sprintf(out,"%lu.%02lus",hundredths/100UL,hundredths%100UL); -} - -/* Open Watcom's DOS conio has no textattr()/textcolor() -- it offers only - cprintf/cputs/getch and friends -- and ANSI escapes are not interpreted on - the installed FreeDOS console. So let cprintf lay the line out (it scrolls - correctly) and then repaint the attribute bytes of the cells it just wrote. - The cursor sits at column 0 of the row after the line, which is what lets us - find those cells without tracking scrolling ourselves. */ -#define TIMESTAMP_WIDTH 9 -static void colorize(unsigned total,int attr) { - unsigned char __far *vram; unsigned cols,used,row,col,start,k; - if(*(unsigned char __far *)MK_FP(0x0040,0x0049)==7) return; /* MDA: no color */ - cols=*(unsigned __far *)MK_FP(0x0040,0x004A); - if(cols<40||cols>132) cols=80; - used=(total+cols-1)/cols; if(!used) used=1; - row=*(unsigned char __far *)MK_FP(0x0040,0x0051); - if(row262144L){remove("C:\\TCPAGENT.OLD");rename("C:\\TCPAGENT.LOG","C:\\TCPAGENT.OLD");} - f=fopen("C:\\TCPAGENT.LOG","a"); - if(f){fputs("--- TCPAGENT start ---\n",f);fclose(f);} -} -/* PUT completes either here in command_put (zero length) or in the receive loop - once the body arrives, so keep the one line both paths emit in one place. */ -static void put_finished(void) { - char elapsed[24]; elapsed_text(put_started,elapsed); - log_line(0x0A,"< PUT %s OK %s",put_path,elapsed); -} - -void __interrupt __far ctrl_break(void) { stop_requested=1; } -void __interrupt __far ctrl_c(void) { stop_requested=1; } - -static void drive(void) { - PACKET_PROCESS_MULT(5); - Arp::driveArp(); - Tcp::drivePackets(); -} - -static int send_all(const void *buffer, unsigned length) { - const uint8_t *p=(const uint8_t *)buffer; - unsigned sent=0; - int rc; - while(sentsend((uint8_t *)(p+sent),length-sent); - if(rc>0) sent+=(unsigned)rc; - else if(rc<0 || socketp->isRemoteClosed()) return -1; - } - return sent==length ? 0 : -1; -} - -static void write_text(const char *s) { send_all(s,strlen(s)); } -static int hexval(int c) { - if(c>='0'&&c<='9') return c-'0'; - if(c>='A'&&c<='F') return c-'A'+10; - if(c>='a'&&c<='f') return c-'a'+10; - return -1; -} -static int decode_hex(const char *src,unsigned char *dst,int cap) { - int n=0,hi,lo; - while(*src&&src[1]) { - if(n>=cap) return -1; - hi=hexval((unsigned char)src[0]); lo=hexval((unsigned char)src[1]); - if(hi<0||lo<0) return -1; - dst[n++]=(unsigned char)((hi<<4)|lo); src+=2; - } - return *src ? -1 : n; -} -static void write_hex(const unsigned char *src,unsigned count) { - static const char digits[]="0123456789ABCDEF"; - char out[256]; unsigned i,n; - while(count) { - n=count>sizeof(out)/2 ? sizeof(out)/2 : count; - for(i=0;i>4]; out[i*2+1]=digits[src[i]&15]; } - send_all(out,n*2); src+=n; count-=n; - } -} -static void ok_data(const unsigned char *src,unsigned count) { - write_text("OK "); write_hex(src,count); write_text("\r\n"); -} -static void error_text(const char *s) { - write_text("ERR "); write_hex((const unsigned char *)s,strlen(s)); write_text("\r\n"); -} -static int decode_path(const char *hex,char *path,int cap) { - int n=decode_hex(hex,(unsigned char *)path,cap-1); - if(n<0) return 0; path[n]='\0'; return 1; -} -static void command_read(char *args) { - char path[260],*off=strchr(args,' '); FILE *f; long pos; size_t count; int eof; - if(!off){log_line(0x0C,"< READ ERR missing offset");error_text("READ requires path and offset");return;} *off++='\0'; - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< READ ERR bad path encoding");error_text("Invalid path encoding");return;} - pos=atol(off); log_line(0x0B,"> READ %s @%ld",path,pos); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< READ ERR cannot open");error_text("Cannot open file");return;} - if(fseek(f,pos,SEEK_SET)){fclose(f);log_line(0x0C,"< READ ERR cannot seek");error_text("Cannot seek file");return;} - count=fread(data,1,CHUNK_SIZE,f); eof=count WRITE %s %c %dB",path,mode[0]=='A'?'A':'T',count); - f=fopen(path,mode[0]=='A'?"ab":"wb"); if(!f){log_line(0x0C,"< WRITE ERR cannot open");error_text("Cannot write file");return;} - if(count&&fwrite(data,1,count,f)!=(size_t)count){fclose(f);log_line(0x0C,"< WRITE ERR short write");error_text("Short write");return;} - fclose(f); log_line(0x0A,"< WRITE OK"); ok_data((const unsigned char *)"",0); -} -static void command_put(char *args) { - char path[260],*length_text=strchr(args,' '); - if(!length_text){log_line(0x0C,"< PUT ERR missing length");error_text("PUT requires path and length");return;} - *length_text++='\0'; - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< PUT ERR bad path encoding");error_text("Invalid path encoding");return;} - put_remaining=strtoul(length_text,0,10); strcpy(put_path,path); put_started=ticks(); - log_line(0x0B,"> PUT %s %luB",put_path,put_remaining); - put_file=fopen(path,"wb"); - if(!put_file){put_remaining=0;log_line(0x0C,"< PUT %s ERR cannot open",put_path);error_text("Cannot write file");return;} - if(!put_remaining){fclose(put_file);put_file=0;put_finished();ok_data((const unsigned char *)"",0);} -} -static void command_get(char *args) { - char path[260],elapsed[24]; FILE *f; long length; size_t count; unsigned long started=ticks(); - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< GET ERR bad path encoding");error_text("Invalid path encoding");return;} - log_line(0x0B,"> GET %s",path); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< GET %s ERR cannot open",path);error_text("Cannot open file");return;} - fseek(f,0,SEEK_END); length=ftell(f); fseek(f,0,SEEK_SET); - sprintf(linebuf,"DATA %ld\r\n",length); write_text(linebuf); - while((count=fread(data,1,CHUNK_SIZE,f))>0) if(send_all(data,count)<0)break; - fclose(f); elapsed_text(started,elapsed); - log_line(0x0A,"< GET OK %ldB %s",length,elapsed); -} -static void command_hash(char *args) { - char path[260],elapsed[24]; FILE *f; size_t count; unsigned i; - unsigned long length=0,hash=2166136261UL,started=ticks(); - if(!decode_path(args,path,sizeof(path))){log_line(0x0C,"< HASH ERR bad path encoding");error_text("Invalid path encoding");return;} - log_line(0x0B,"> HASH %s",path); - f=fopen(path,"rb"); if(!f){log_line(0x0C,"< HASH %s ERR cannot open",path);error_text("Cannot open file");return;} - while((count=fread(data,1,CHUNK_SIZE,f))>0){length+=(unsigned long)count;for(i=0;i LIST %s",path); - strcpy(pattern,path); if(pattern[0]&&pattern[strlen(pattern)-1]!='\\') strcat(pattern,"\\"); strcat(pattern,"*.*"); - rc=_dos_findfirst(pattern,_A_NORMAL|_A_RDONLY|_A_HIDDEN|_A_SYSTEM|_A_SUBDIR|_A_ARCH,&found); - while(rc==0) { char entry[100]; int len; - if(strcmp(found.name,".")&&strcmp(found.name,"..")) { sprintf(entry,"%s\t%lu\t%s\n",found.name,found.size,(found.attrib&_A_SUBDIR)?"DIR":"FILE"); len=strlen(entry); if(used+(unsigned)len>=sizeof(output)) {truncated=1;break;} memcpy(output+used,entry,len); used+=(unsigned)len; ++entries; } - rc=_dos_findnext(&found); - } - log_line(truncated?0x0E:0x0A,"< LIST %u entries%s",entries,truncated?" (truncated)":""); - ok_data((unsigned char *)output,used); -} -static void command_exec(char *args) { - char command[700],elapsed[24]; const char *tmp="C:\\PIEXEC.TMP"; FILE *f; - int n,code=-1,fd=-1,save1=-1,save2=-1; long length=0; size_t count; unsigned long started; - n=decode_hex(args,(unsigned char *)command,sizeof(command)-1); if(n<0){error_text("Invalid command encoding");return;} command[n]='\0'; - started=ticks(); log_line(0x0E,"> EXEC %.640s",command); - fflush(stdout); fflush(stderr); - save1=dup(1); save2=dup(2); - fd=open(tmp,O_CREAT|O_TRUNC|O_WRONLY|O_BINARY,S_IREAD|S_IWRITE); - if(save1<0||save2<0||fd<0||dup2(fd,1)<0||dup2(fd,2)<0) { - if(fd>=0)close(fd); - if(save1>=0){dup2(save1,1);close(save1);} - if(save2>=0){dup2(save2,2);close(save2);} - log_line(0x0C,"< EXEC ERR redirect failed"); error_text("Cannot capture command output"); return; - } - close(fd); code=system(command); fflush(stdout); fflush(stderr); - dup2(save1,1); dup2(save2,2); close(save1); close(save2); - f=fopen(tmp,"rb"); - if(f){fseek(f,0,SEEK_END);length=ftell(f);fseek(f,0,SEEK_SET);} - elapsed_text(started,elapsed); - log_line(code?0x0C:0x0A,"< EXEC exit=%d %ldB %s",code,length,elapsed); - sprintf(linebuf,"RESULT %d %ld 0\r\n",code,length); write_text(linebuf); - while(f&&(count=fread(data,1,CHUNK_SIZE,f))>0)if(send_all(data,count)<0)break; - if(f)fclose(f); remove(tmp); -} -static void process_line(char *line) { - char *cmd=line,*args=strchr(line,' '); if(args)*args++='\0';else args=cmd+strlen(cmd); - /* PING is deliberately not logged: wait-ready polls it twice a second. */ - if(!strcmp(cmd,"PING"))ok_data((const unsigned char *)"PONG",4); - else if(!strcmp(cmd,"READ"))command_read(args); - else if(!strcmp(cmd,"WRITE"))command_write(args); - else if(!strcmp(cmd,"PUT"))command_put(args); - else if(!strcmp(cmd,"GET"))command_get(args); - else if(!strcmp(cmd,"HASH"))command_hash(args); - else if(!strcmp(cmd,"LIST"))command_list(args); - else if(!strcmp(cmd,"EXEC"))command_exec(args); - else if(!strcmp(cmd,"QUIT")){log_line(0x07,"* QUIT received");ok_data((const unsigned char *)"BYE",3);stop_requested=1;} - else { char message[96]; sprintf(message,"Unknown command: %.70s",cmd); log_line(0x0C,"< ERR %s",message); error_text(message); } -} -static int connect_host(void) { - IpAddr_t host={10,0,2,2}; int8_t rc; - socketp=TcpSocketMgr::getSocket(); if(!socketp)return -1; - socketp->setRecvBuffer(RECV_SIZE); - rc=socketp->connect(LOCAL_PORT,host,SERVER_PORT,10000); - while(rc==0&&!socketp->isConnectComplete()&&!socketp->isRemoteClosed()&&!stop_requested)drive(); - if(socketp->isRemoteClosed()){TcpSocketMgr::freeSocket(socketp);socketp=0;return -1;} - return 0; -} -int main(void) { - int used,rc; uint16_t key; unsigned attempts=0; - init_log(); - log_line(0x0B,"* TCPAGENT starting; Alt-X returns to DOS"); - if(Utils::parseEnv()!=0){log_line(0x0C,"* MTCP configuration error");return 2;} - if(Utils::initStack(1,TCP_SOCKET_RING_SIZE,ctrl_break,ctrl_c)){log_line(0x0C,"* TCP stack initialization error");return 3;} - while(!stop_requested) { - if(!attempts)log_line(0x07,"* connecting 10.0.2.2:%u",SERVER_PORT); - if(connect_host()!=0){unsigned long spins=0;++attempts;if(attempts==1||attempts%10==0)log_line(0x0C,"* connect failed; retry %u",attempts);while(spins++<60000UL&&!stop_requested)drive();continue;} - attempts=0; log_line(0x0A,"* connected; host automation owns console"); - write_text("TCPAGENT READY\r\n"); used=0; - while(!stop_requested&&!socketp->isRemoteClosed()) { - drive(); rc=socketp->recv((uint8_t *)data,CHUNK_SIZE); - if(rc<0)break; - for(int i=0;i>8)==45)stop_requested=1;} - } - socketp->close(); TcpSocketMgr::freeSocket(socketp); socketp=0; - if(!stop_requested)log_line(0x0C,"* link lost; retrying"); - } - log_line(0x07,"* stopped; returning to DOS"); - Utils::endStack(); return 0; -} diff --git a/uv.lock b/uv.lock index c7b989e..500d600 100644 --- a/uv.lock +++ b/uv.lock @@ -2,152 +2,6 @@ version = 1 revision = 1 requires-python = ">=3.12" -[[package]] -name = "antlr4-python3-runtime" -version = "4.9.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034 } - -[[package]] -name = "certifi" -version = "2026.7.22" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983 }, -] - -[[package]] -name = "charset-normalizer" -version = "3.5.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456 }, - { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530 }, - { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200 }, - { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222 }, - { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951 }, - { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801 }, - { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070 }, - { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110 }, - { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836 }, - { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712 }, - { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977 }, - { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207 }, - { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562 }, - { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507 }, - { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551 }, - { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700 }, - { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584 }, - { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359 }, - { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464 }, - { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676 }, - { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473 }, - { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156 }, - { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246 }, - { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660 }, - { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354 }, - { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638 }, - { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583 }, - { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038 }, - { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677 }, - { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491 }, - { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196 }, - { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660 }, - { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618 }, - { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362 }, - { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755 }, - { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295 }, - { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856 }, - { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318 }, - { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897 }, - { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848 }, - { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163 }, - { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823 }, - { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458 }, - { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717 }, - { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111 }, - { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128 }, - { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240 }, - { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282 }, - { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597 }, - { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376 }, - { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715 }, - { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848 }, - { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521 }, - { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054 }, - { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580 }, - { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325 }, - { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175 }, - { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123 }, - { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682 }, - { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826 }, - { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861 }, - { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758 }, - { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950 }, - { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329 }, - { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137 }, - { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820 }, - { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504 }, - { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087 }, - { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269 }, - { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766 }, - { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814 }, - { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074 }, - { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476 }, - { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115 }, - { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048 }, - { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997 }, - { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014 }, - { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174 }, - { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361 }, - { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143 }, - { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086 }, - { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231 }, - { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546 }, - { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033 }, - { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045 }, - { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866 }, - { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932 }, - { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320 }, - { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174 }, - { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126 }, - { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441 }, - { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742 }, - { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298 }, - { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500 }, - { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888 }, - { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243 }, - { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871 }, - { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580 }, - { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807 }, - { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083 }, - { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317 }, - { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173 }, - { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960 }, - { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186 }, - { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947 }, - { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909 }, - { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467 }, - { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057 }, - { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930 }, - { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822 }, - { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037 }, - { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097 }, - { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166 }, - { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821 }, - { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529 }, - { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348 }, - { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234 }, - { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917 }, - { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846 }, - { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216 }, - { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764 }, - { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318 }, - { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658 }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -157,51 +11,16 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] -[[package]] -name = "colorlog" -version = "6.12.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8c/55/ba79756cb90c8d69d599d57785398ac87bba7b19c80e87f4e8a562197c93/colorlog-6.12.0.tar.gz", hash = "sha256:2a7924c1dadf18b22a0eb8b06d1c7b01d5341707ec1641eb6fcc4fde0c3e8e5f", size = 18151 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/19/0b6647bf5e331521e55d2b63bfbdc210bd9cd605189273f03614a05f702d/colorlog-6.12.0-py3-none-any.whl", hash = "sha256:30d392604e9110045a2c2aeefc27d7a017abbab63f3a8aee594eac0801df784e", size = 12239 }, -] - [[package]] name = "ferrolang" version = "0.1.0" source = { editable = "." } dependencies = [ - { name = "onnxruntime" }, { name = "pytest" }, - { name = "rapidocr" }, ] [package.metadata] -requires-dist = [ - { name = "onnxruntime", specifier = ">=1.28.0" }, - { name = "pytest", specifier = ">=9.0.0" }, - { name = "rapidocr", specifier = ">=3.9.2" }, -] - -[[package]] -name = "flatbuffers" -version = "25.12.19" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, -] - -[[package]] -name = "idna" -version = "3.18" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455 }, -] +requires-dist = [{ name = "pytest", specifier = ">=9.0.0" }] [[package]] name = "iniconfig" @@ -212,143 +31,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, ] -[[package]] -name = "numpy" -version = "2.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693 }, - { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109 }, - { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202 }, - { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736 }, - { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696 }, - { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264 }, - { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396 }, - { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044 }, - { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817 }, - { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674 }, - { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131 }, - { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595 }, - { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845 }, - { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880 }, - { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264 }, - { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566 }, - { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995 }, - { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511 }, - { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609 }, - { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204 }, - { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532 }, - { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725 }, - { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180 }, - { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878 }, - { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922 }, - { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168 }, - { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501 }, - { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701 }, - { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065 }, - { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031 }, - { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028 }, - { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627 }, - { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414 }, - { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967 }, - { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874 }, - { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276 }, - { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154 }, - { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909 }, - { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685 }, - { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181 }, - { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085 }, - { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971 }, - { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306 }, - { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274 }, - { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846 }, - { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892 }, - { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309 }, - { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850 }, - { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664 }, - { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749 }, - { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495 }, - { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696 }, - { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324 }, - { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466 }, - { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947 }, - { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331 }, - { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336 }, - { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387 }, - { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096 }, - { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730 }, - { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686 }, - { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727 }, - { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775 }, - { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559 }, - { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103 }, -] - -[[package]] -name = "omegaconf" -version = "2.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "antlr4-python3-runtime" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ce/3d/e4b57b8d9008c6ebe0d5eff901f91d5700cf7bdb8c8863df817463a7fd5e/omegaconf-2.3.1.tar.gz", hash = "sha256:e5e7de64aeebeddaf8e6d3f7a783b32ac2a01c0fbd9c878012caecb891a1f42a", size = 3298472 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/0e/152509871bf30df6fc38569f52a2db9b55dd41aae957adae50a053ac7778/omegaconf-2.3.1-py3-none-any.whl", hash = "sha256:3d701d14e9a8828f1edd28bb70b725908b34277cdd72cf7d6a83f94dadc6b6a0", size = 79502 }, -] - -[[package]] -name = "onnxruntime" -version = "1.28.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flatbuffers" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "protobuf" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/98/f8/dcbe7700dca82fa540035abd3c868fe5ad0f86af00b9a3db7c2e27d15c7d/onnxruntime-1.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:26ff0fdd06efb6c155bae95387a09db1a2be89c7a03e4d0bffd5a171cc2826da", size = 19141362 }, - { url = "https://files.pythonhosted.org/packages/28/5b/1d77e62097fdbe07e2dc827f389b1c4c0c275f6fab0369a8f46d2461af27/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e81a23df16e7acb9d51b06d30cc098e49315ef9180f97bc2221d167b4b04d9c", size = 17050628 }, - { url = "https://files.pythonhosted.org/packages/95/df/5486ab03e9be288d5268867054c8b04bebcf95bfd12e801c05cc67703dab/onnxruntime-1.28.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a83bdb70d143cede762b677789bf2a7acca54b3fb82565601d5c30695aa933c", size = 19214257 }, - { url = "https://files.pythonhosted.org/packages/3e/3b/986ca67c274932ba9ac5332fb10de56f643dfd433c74e33f8ae8f847cf24/onnxruntime-1.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:c35064f9b3c43c81c5d5d282091401d0f1ff22796d93ccade4ea2ece5e137ab8", size = 13755036 }, - { url = "https://files.pythonhosted.org/packages/1d/46/059dba81d46c6ba88e0c2d1c64321ac8098847d678423300a183d42ecbd6/onnxruntime-1.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:e02feeb0165c5f13b4cc954738078d59b90128516ac12b671ee24a530242bf02", size = 13454462 }, - { url = "https://files.pythonhosted.org/packages/9c/12/3807e2b17d9eb71d3cb78ed2ba76869b05c637c9b9d6112e636098b0c97a/onnxruntime-1.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:31410f544674f534c2f27348af52ef81682ca9c8719154bf4d48f0ef23823b1e", size = 19141759 }, - { url = "https://files.pythonhosted.org/packages/c0/23/b46045c3bf67a9cf54c12f5df0f018a422c65fbb9d6072b10071bebfaae2/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f649dd6f6452d12a8059888aa489fe519e062e18793dac72b9efa0f9fdb64135", size = 17049339 }, - { url = "https://files.pythonhosted.org/packages/78/b6/8c5396e7894e77c5a7d1e026f3acb9dd39c4b5644e412e37a0055eaa3bc5/onnxruntime-1.28.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fa221d669282bd8f582708ce4c96010a7e9fb0661f9006b37fe2fedafb73fe", size = 19214329 }, - { url = "https://files.pythonhosted.org/packages/56/f1/51225c202edba4dfc94e1ea03f3d78f1aaf307da75fd792c0ce1946b2514/onnxruntime-1.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:1a1a19175464665c9b8d50bc916f216cc0b569110045b7bbca8f9f290b186f58", size = 13755033 }, - { url = "https://files.pythonhosted.org/packages/f4/db/f59f715edfdd96a051f32b5ef0e680a20a8755d4ecd75f63090e960e347a/onnxruntime-1.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:cfab507abe09d6ffeb817eee07944d452fdc0b00fdcef34cab4db10a45e378c7", size = 13454175 }, - { url = "https://files.pythonhosted.org/packages/47/28/810314fa88647af9f4cdaf438a30ad1cfebebb53ded55499232d7a0094e6/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac301f53b1930402fc46c368e268acfed02f3207272aaff05070d7e09f96f031", size = 17057307 }, - { url = "https://files.pythonhosted.org/packages/3d/cc/9e9f193cc0f29f263a8f09ec08487aed6c96ee856d5fd77da32a425c1949/onnxruntime-1.28.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7f022a1103cae591c75fc4565589a515f2ddd14a6ac8e8a05812dfeda142e28", size = 19222954 }, - { url = "https://files.pythonhosted.org/packages/4e/eb/952314c451d9463e5c9aed9978eec76cf32930d407d9ab8700dd0f4ea1ea/onnxruntime-1.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:8adff67a3f28257b37cfe945a7e952e4122666aa8c91a0380862e9fd4c2ed19f", size = 19143748 }, - { url = "https://files.pythonhosted.org/packages/3f/e9/139180b4dd810329aaa42c238b4e6383c906202d98609ae29d66eb7c32b1/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc2565e487b4896fb988d6383577d875d958e071fc5f6c3550bd5d02ae98264b", size = 17051950 }, - { url = "https://files.pythonhosted.org/packages/03/88/9432428273356ad3c8aa01f52c1b3e7f53c4c0192748f41ad983872b436b/onnxruntime-1.28.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6afdc83f1317c136e92fc29f5ee9f058de59d87c0b22cee3fdbfbaa0ccc2098a", size = 19214924 }, - { url = "https://files.pythonhosted.org/packages/bb/e2/6feb3a43517aaf2b1bf7e46897ba5eb81a29717f7d7901420614d5ee4653/onnxruntime-1.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:f2a3b9e30ce880d4ca54999cb313569e36da4f62eefe25f87be18f43e9a3a4d5", size = 14093738 }, - { url = "https://files.pythonhosted.org/packages/fc/8f/83974a1e201dc2e58e5e7111bcaeb1ca2413e9c41f505d26419ee9e3dddf/onnxruntime-1.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:07fb3cbe990d6bf0ab3c22bfbbfb0e314151266046ea6edb4a07f556b4258c5f", size = 13821117 }, - { url = "https://files.pythonhosted.org/packages/0d/83/00e606bc25c756d76a267370c39b7516ad52f9cf134d7ff2bff8b6108bc4/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e562d6e36a749f6764481c0ddb0f2af3d0b5a3c164291361d08803c557f369af", size = 17055518 }, - { url = "https://files.pythonhosted.org/packages/94/a9/68707e1ce345cbdbcd4df65932ebc82a673e917d63eda0007ebcff948691/onnxruntime-1.28.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f6e92367ddce1e4d33cf295024f40192be6c6171a09208f515ba169ced06c8e", size = 19222976 }, -] - -[[package]] -name = "opencv-python" -version = "5.0.0.93" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443 }, - { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755 }, - { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064 }, - { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711 }, - { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576 }, - { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032 }, - { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734 }, - { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345 }, -] - [[package]] name = "packaging" version = "26.3" @@ -358,77 +40,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956 }, ] -[[package]] -name = "pillow" -version = "12.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969 }, - { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323 }, - { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838 }, - { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830 }, - { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383 }, - { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934 }, - { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684 }, - { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137 }, - { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267 }, - { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684 }, - { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487 }, - { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433 }, - { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889 }, - { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109 }, - { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736 }, - { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129 }, - { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562 }, - { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439 }, - { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287 }, - { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691 }, - { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185 }, - { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736 }, - { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435 }, - { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262 }, - { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344 }, - { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131 }, - { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757 }, - { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962 }, - { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171 }, - { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116 }, - { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209 }, - { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707 }, - { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995 }, - { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503 }, - { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956 }, - { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855 }, - { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642 }, - { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281 }, - { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716 }, - { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125 }, - { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939 }, - { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506 }, - { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063 }, - { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549 }, - { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331 }, - { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370 }, - { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147 }, - { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659 }, - { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439 }, - { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577 }, - { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394 }, - { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375 }, - { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048 }, - { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006 }, - { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509 }, - { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167 }, - { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237 }, - { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047 }, - { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440 }, - { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895 }, - { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384 }, - { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537 }, - { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491 }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -438,51 +49,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, ] -[[package]] -name = "protobuf" -version = "7.35.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226 }, - { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847 }, - { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030 }, - { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130 }, - { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945 }, - { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996 }, - { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659 }, -] - -[[package]] -name = "pyclipper" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676 }, - { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458 }, - { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235 }, - { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388 }, - { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169 }, - { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619 }, - { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342 }, - { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839 }, - { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142 }, - { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789 }, - { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817 }, - { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007 }, - { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167 }, - { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966 }, - { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216 }, - { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198 }, - { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951 }, - { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782 }, - { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880 }, - { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706 }, - { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308 }, - { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608 }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -507,166 +73,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa wheels = [ { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536 }, ] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669 }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252 }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081 }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159 }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626 }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613 }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115 }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427 }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090 }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246 }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814 }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809 }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454 }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355 }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175 }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228 }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194 }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429 }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912 }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108 }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641 }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901 }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132 }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261 }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272 }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923 }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062 }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341 }, -] - -[[package]] -name = "rapidocr" -version = "3.9.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorlog" }, - { name = "numpy" }, - { name = "omegaconf" }, - { name = "opencv-python" }, - { name = "pillow" }, - { name = "pyclipper" }, - { name = "pyyaml" }, - { name = "requests" }, - { name = "shapely" }, - { name = "six" }, - { name = "tqdm" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/55/ed/0ee9b9281986974be9d2406ae0134c8d7c91d2fc613f16ffda9701eeda6f/rapidocr-3.9.2-py3-none-any.whl", hash = "sha256:04d6b8d151f823d930bd91910555f57bea897c0c44fa6794267b94cf9c1ef9a0", size = 27275208 }, -] - -[[package]] -name = "requests" -version = "2.34.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "charset-normalizer" }, - { name = "idna" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075 }, -] - -[[package]] -name = "shapely" -version = "2.1.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550 }, - { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556 }, - { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308 }, - { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844 }, - { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842 }, - { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714 }, - { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745 }, - { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861 }, - { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644 }, - { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887 }, - { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931 }, - { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855 }, - { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960 }, - { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851 }, - { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890 }, - { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151 }, - { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130 }, - { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802 }, - { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460 }, - { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223 }, - { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760 }, - { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078 }, - { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178 }, - { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756 }, - { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290 }, - { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463 }, - { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145 }, - { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806 }, - { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803 }, - { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301 }, - { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247 }, - { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019 }, - { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137 }, - { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884 }, - { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320 }, - { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931 }, - { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406 }, - { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511 }, - { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607 }, - { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682 }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, -] - -[[package]] -name = "tqdm" -version = "4.70.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184 }, -] - -[[package]] -name = "urllib3" -version = "2.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087 }, -] From 5529e3dc14322672bbc78e2c455677808d003162 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 22:23:41 +0900 Subject: [PATCH 089/184] test: prove --no-checks removes the bounds check The bounds-no-checks cases emitted and built but never ran, so they only proved that --no-checks produces compilable C -- not that it removes the check, which is the entire point of the flag. A run case could not simply be appended. BOUNDS.FE returns the out-of-bounds element directly, so with checks removed its exit code is whatever sits past the array on the stack and there is no correct status to assert. Asserting on the generated C instead does not work either: emit_c.c defines fe_trap_bounds unconditionally and --no-checks only suppresses the call sites. Add NOCHK.FE, which reads one element past a [2]i32 and returns x - x. That is 0 for whatever garbage the unchecked read produced, so the same source has a defined outcome both ways: compiled with checks it must trap, compiled with --no-checks it must run to completion and exit 0. Register both halves and drop the two BOUNDS-N cases they supersede. Verified in DOSBox-X: 155 passed, including m3-nochk-trap failing as expected and m3-nochk-off-run succeeding. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- fec/tests/m3/nochk.fe | 7 +++++++ src/ferrolang_vm/registry.py | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 fec/tests/m3/nochk.fe diff --git a/fec/tests/m3/nochk.fe b/fec/tests/m3/nochk.fe new file mode 100644 index 0000000..e8e62f3 --- /dev/null +++ b/fec/tests/m3/nochk.fe @@ -0,0 +1,7 @@ +unit m3_no_checks; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let x: i32 = a[2]; + return x - x; +} diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index 5478bbc..74d129a 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -146,10 +146,18 @@ CASES: list[Case] = [ # These two must trap at runtime: the bounds check is the feature under test. *_triple(3, "bounds", M3, run_suffix="trap", run_ok=False), *_triple(3, "slcbound", M3, run_suffix="trap", run_ok=False), - _case(3, "bounds-no-checks-emit", - _emit(_fe(M3, "bounds"), f"{M3}\\BOUNDS-N.C", flags=("--no-checks",))), - _case(3, "bounds-no-checks-build", - _wcl(f"{M3}\\BOUNDS-N.EXE", f"{M3}\\BOUNDS-N.C")), + # --no-checks is proved by a differential on one source. NOCHK.FE reads one + # element past a [2]i32 and returns x - x, which is 0 whatever garbage the + # unchecked read produced: compiled with checks it must trap, compiled with + # --no-checks it must run to completion. BOUNDS.FE cannot serve as the + # unchecked half because it returns the out-of-bounds value directly, so its + # exit code would be whatever happens to sit past the array on the stack. + *_triple(3, "nochk", M3, run_suffix="trap", run_ok=False), + _case(3, "nochk-off-emit", + _emit(_fe(M3, "nochk"), f"{M3}\\NOCHK-N.C", flags=("--no-checks",))), + _case(3, "nochk-off-build", + _wcl(f"{M3}\\NOCHK-N.EXE", f"{M3}\\NOCHK-N.C")), + _case(3, "nochk-off-run", f"{M3}\\NOCHK-N.EXE"), *_rejects(3, M3, ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", "badfield", "badindex"), suffix="reject"), From 8b7d6a8d09dc951f4f353e0fe8c7e0a186962a22 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 23:02:28 +0900 Subject: [PATCH 090/184] dev: make DOS build failures diagnosable Reviewing an M7 branch took six DOSBox-X runs to find three build blockers that each take a second to explain. The runner threw away everything needed to see them. Capture the compiler build's output. Case commands were redirected to RESULTS\.LOG but `call BUILD.BAT` was not, so the step that fails first and blocks every case left only BUILD.FAIL containing the string "FAIL". The twelve wcl invocations inside it were invisible; finding "Unable to open src\emit_c_m7.c" meant hand-editing build-dos.bat to add a redirect and re-running the VM. Record exit codes. The batch collapsed every outcome to `if errorlevel 1`, so a compiler that aborted and one that exited 1 with a diagnostic were the same FAIL. RC.BAT now walks a descending errorlevel ladder into RESULTS\.RC and the host derives pass/fail from it, which immediately separates an ordinary rejection (1) from a trap (255). Note the space in `echo 0 >FILE`: without it DOS parses `0>` as a redirect of handle 0. Stop falling back to CONSOLE.LOG. That is DOSBox-X's own log -- display enumeration and INT15 chatter -- so a crashed command reported fifty lines of emulator noise instead of saying it produced no output. Add tools/tests/test_dos_names.py. An over-long source name reaches the DOS build as `Unable to open "src\..."`, which reads as a missing file rather than a name FAT cannot represent, and only after a VM boot and ten object builds. The check runs on the host in 0.03s and flags emit_c_m7.c (9-character stem) on the branch that prompted this. Also pass -k through to pytest so a single case can be re-run without its whole milestone, and print the resolved ROOT at startup: an editable install plus a git worktree will otherwise silently build a different checkout than the one the shell is in. Verified on master: 155 passed, unchanged. Recorded codes are 0 for success, 1 for rejections, 255 for the three bounds traps. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- src/ferrolang_vm/dosboxx.py | 80 +++++++++++++++++++------- src/ferrolang_vm/test_cli.py | 16 ++++-- tools/tests/test_dos_names.py | 72 +++++++++++++++++++++++ tools/tests/test_milestones_dosboxx.py | 5 +- 4 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 tools/tests/test_dos_names.py diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index 58b5ca8..ac94cfd 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -126,12 +126,40 @@ def resolve_tools() -> tuple[Path, Path]: return dosbox, watcom +# ``if errorlevel N`` in DOS tests ``>= N``, so an exact code needs a descending +# ladder. Small values get their own rung because they carry the meaning -- 1 is +# an ordinary compiler error, 3 is Watcom's abort() -- while anything above 8 is +# bucketed to a lower bound, which is enough to tell a crash from a diagnostic. +_RC_LADDER = (255, 128, 64, 32, 16, 8, 7, 6, 5, 4, 3, 2, 1) + + +def _rc_batch() -> str: + """Batch helper that records the previous command's exit code. + + Called as ``call RC.BAT `` right after a case command, since anything + else -- including writing a file -- would clobber ERRORLEVEL first. Note the + space before each ``>``: ``echo 0>FILE`` would parse as a redirect of handle + 0 rather than an echo of "0", so the value is written with a trailing space + and stripped on the host. + """ + lines = ["@echo off"] + lines.extend(f"if errorlevel {value} goto R{value}" for value in _RC_LADDER) + lines.extend(["echo 0 >RESULTS\\%1.RC", "goto END"]) + for value in _RC_LADDER: + lines.extend([f":R{value}", f"echo {value} >RESULTS\\%1.RC", "goto END"]) + lines.extend([":END", ""]) + return "\r\n".join(lines) + + def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: + # The compiler build is the step that fails first and blocks everything after + # it, so its output is captured exactly like a case command's. + build = "call BUILD.BAT" if trace_dos else "call BUILD.BAT > RESULTS\\BUILD.LOG" lines = [ "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", "set WATCOM=W:", "set INCLUDE=W:\\H", "set LIB=W:\\LIB286\\DOS;W:\\LIB286;W:\\LIB386\\DOS;W:\\LIB386", - "call BUILD.BAT", "if not exist BUILD.OK goto BUILDFAIL", + build, "if not exist BUILD.OK goto BUILDFAIL", "echo PASS>RESULTS\\BUILD.RES", ] for index, case in enumerate(cases): @@ -145,19 +173,10 @@ def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: ]) if not trace_dos: command += f" > RESULTS\\{key}.LOG" - lines.append(command) - if case.expect_success: - lines.extend([ - f"if errorlevel 1 goto {key}E", f"echo PASS>RESULTS\\{key}.RES", - f"goto {key}C", f":{key}E", f"echo FAIL>RESULTS\\{key}.RES", - ]) - else: - lines.extend([ - f"if errorlevel 1 goto {key}E", f"echo FAIL>RESULTS\\{key}.RES", - f"goto {key}C", f":{key}E", f"echo PASS>RESULTS\\{key}.RES", - ]) lines.extend([ - f":{key}C", f"if exist *.ERR type *.ERR > RESULTS\\{key}.ERR", + command, + f"call RC.BAT {key}", + f"if exist *.ERR type *.ERR > RESULTS\\{key}.ERR", f"if not exist RESULTS\\{key}.ERR type NUL > RESULTS\\{key}.ERR", ]) lines.extend([ @@ -181,22 +200,40 @@ class SuiteRun: def _key(self, case: Case) -> str: return f"C{self.cases.index(case):03d}" + def rc(self, case: Case) -> int | None: + """Exit code the DOS command reported, or None if it was never recorded. + + Values above 8 are a lower bound; see ``_RC_LADDER``. + """ + path = self.fec / "RESULTS" / f"{self._key(case)}.RC" + if not path.is_file(): + return None + text = path.read_text(encoding="ascii", errors="replace").strip() + return int(text) if text.isdigit() else None + def result(self, case: Case | None = None) -> str: - name = "BUILD" if case is None else self._key(case) - path = self.fec / "RESULTS" / f"{name}.RES" - return path.read_text(encoding="ascii").strip() if path.is_file() else "MISSING" + if case is None: + path = self.fec / "RESULTS" / "BUILD.RES" + return path.read_text(encoding="ascii").strip() if path.is_file() else "MISSING" + code = self.rc(case) + if code is None: + return "MISSING" + return "PASS" if (code == 0) == case.expect_success else "FAIL" def log(self, case: Case | None = None) -> str: name = "BUILD" if case is None else self._key(case) path = self.fec / "RESULTS" / f"{name}.LOG" content = path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" - if content: + if content.strip(): return content errors = sorted(self.fec.glob("*.ERR")) - if errors: - return "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in errors) - console = self.root / "CONSOLE.LOG" - return console.read_text(encoding="utf-8", errors="replace") if console.is_file() else "" + joined = "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in errors) + if joined.strip(): + return joined + # Deliberately not falling back to CONSOLE.LOG: that is the emulator's own + # log (display enumeration, INT15 chatter) and burying one useful line in + # it reads as output when there was none. Use --dos-log to see it. + return "(no DOS output captured; the command wrote nothing before exiting)" def err(self, case: Case) -> str: path = self.fec / "RESULTS" / f"{self._key(case)}.ERR" @@ -229,6 +266,7 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, _batch(cases, show_dos=show_dos, trace_dos=trace_dos), encoding="ascii", newline="", ) + (fec / "RC.BAT").write_text(_rc_batch(), encoding="ascii", newline="") command = [str(dosbox)] if not show_dos: command.append("-silent") diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py index e20f744..075a525 100644 --- a/src/ferrolang_vm/test_cli.py +++ b/src/ferrolang_vm/test_cli.py @@ -35,7 +35,9 @@ def main() -> int: help="print the captured DOS console after the run") run.add_argument("--trace-dos", action="store_true", help="do not redirect case command output") - args = parser.parse_args() + run.add_argument("-k", dest="select", metavar="EXPR", + help="run only cases whose id matches this pytest -k expression") + args, extra = parser.parse_known_args() try: if args.command == "setup": dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) @@ -55,12 +57,18 @@ def main() -> int: if enabled: os.environ[name] = "1" import pytest - test_file = os.fspath( - Path(__file__).resolve().parents[2] / "tools" / "tests" / "test_milestones_dosboxx.py" - ) + from .paths import ROOT + # The package can be imported from a different checkout than the one the + # shell is sitting in -- an editable install plus a git worktree is enough + # to silently build and test the wrong tree. Say which tree this is. + print(f"ferro-test: building {ROOT}", file=sys.stderr) + test_file = os.fspath(ROOT / "tools" / "tests" / "test_milestones_dosboxx.py") pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"] + if args.select: + pytest_args.extend(["-k", args.select]) if args.dos_log: pytest_args.append("-s") + pytest_args.extend(extra) return int(pytest.main(pytest_args)) except (DosboxError, ValueError) as exc: print(f"ferro-test: {exc}", file=sys.stderr) diff --git a/tools/tests/test_dos_names.py b/tools/tests/test_dos_names.py new file mode 100644 index 0000000..2daf1d5 --- /dev/null +++ b/tools/tests/test_dos_names.py @@ -0,0 +1,72 @@ +"""Host-side checks for constraints the DOS toolchain enforces far too late. + +Everything here runs without DOSBox-X. The point is to fail in a tenth of a +second with the offending name, instead of after a DOSBox-X boot and ten +successful object builds -- and with a message that says what is actually wrong. +A 9-character source name reaches the DOS build as ``Unable to open "src\\x.c"``, +which reads as a missing file rather than a name that cannot be represented. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +from ferrolang_vm.paths import ROOT +from ferrolang_vm.registry import CASES + +# The runner copies these onto a FAT filesystem, where a name is at most eight +# characters plus a three-character extension. +COPIED_TREES = ("fec/src", "fec/std", "fec/tests") + + +def _offenders(root: Path) -> list[str]: + bad = [] + for path in sorted(root.rglob("*")): + name = path.name + if name.startswith("."): + continue + stem, _, suffix = name.rpartition(".") if "." in name else (name, "", "") + if len(stem) > 8 or len(suffix) > 3: + bad.append(f"{path.relative_to(ROOT).as_posix()} (stem {len(stem)}, ext {len(suffix)})") + return bad + + +@pytest.mark.parametrize("tree", COPIED_TREES) +def test_copied_files_fit_dos_8_3(tree: str) -> None: + root = ROOT / tree + if not root.is_dir(): + pytest.skip(f"{tree} is absent") + bad = _offenders(root) + assert not bad, ( + f"{len(bad)} name(s) under {tree} cannot be represented on the DOS side.\n" + "The DOS build will report them as missing files, not as long names:\n " + + "\n ".join(bad) + ) + + +def test_registry_paths_exist_on_the_host() -> None: + """Every ``.FE`` a case names must exist, matched case-insensitively. + + DOS is case-insensitive, so a registry typo survives until the command runs + inside the VM and fails with a message about the wrong thing. + """ + available = { + path.relative_to(ROOT / "fec").as_posix().upper() + for path in (ROOT / "fec").rglob("*.fe") + } + available |= { + path.relative_to(ROOT / "fec").as_posix().upper() + for path in (ROOT / "fec").rglob("*.FE") + } + missing = [] + for case in CASES: + for token in case.command.split(): + if not token.upper().endswith(".FE"): + continue + wanted = token.replace("\\", "/").upper() + if wanted.startswith("STD/"): + wanted = f"STD/{wanted[4:]}" + if not any(entry.endswith(wanted) for entry in available): + missing.append(f"{case.id}: {token}") + assert not missing, "registry names fixtures that do not exist:\n " + "\n ".join(missing) diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py index c386b62..8b8ae3f 100644 --- a/tools/tests/test_milestones_dosboxx.py +++ b/tools/tests/test_milestones_dosboxx.py @@ -46,7 +46,10 @@ def test_milestone_case(case: Case, suite_run: SuiteRun) -> None: warning_lines = [line for line in err.splitlines() if "warning" in line.lower()] if warning_lines: warnings.warn("\n".join(warning_lines), stacklevel=1) + code = suite_run.rc(case) assert result == "PASS", ( - f"DOS command: {case.command}\nExpected success: {case.expect_success}\n" + f"DOS command: {case.command}\n" + f"Expected success: {case.expect_success}\n" + f"Exit code: {'not recorded' if code is None else code}\n" f"{suite_run.log(case)}\n{err}" ) From 7a254472816277210278a0fd6221d2209e2ddb68 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 23:02:43 +0900 Subject: [PATCH 091/184] trial: unblock the DOS build (local only, not for merge) --- fec/Makefile | 2 +- fec/build-dos.bat | 2 +- fec/src/check.c | 1 + fec/src/check_m7.c | 2 ++ fec/src/{emit_c_m7.c => emitcm7.c} | 0 src/ferrolang_vm/registry.py | 12 ++++++++++++ 6 files changed, 17 insertions(+), 2 deletions(-) rename fec/src/{emit_c_m7.c => emitcm7.c} (100%) diff --git a/fec/Makefile b/fec/Makefile index 21a8e3d..cd71ce6 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -std=c89 CPPFLAGS ?= -Isrc -SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check_m7.c src/lower.c src/emit_c_m7.c src/driver.c +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check_m7.c src/lower.c src/emitcm7.c src/driver.c OBJ = $(SRC:.c=.o) .PHONY: all clean dos-build diff --git a/fec/build-dos.bat b/fec/build-dos.bat index ecde577..104d28f 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -32,7 +32,7 @@ if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lower.obj src\lower.c if errorlevel 1 goto build_fail rem Use an unambiguous short object name for the M7 emitter source. -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emit_c_m7.c +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emitcm7.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=driver.obj src\driver.c if errorlevel 1 goto build_fail diff --git a/fec/src/check.c b/fec/src/check.c index 01a9396..1258857 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -811,6 +811,7 @@ static FeType *check_index(FeCheckerState *s, FeNode *n) static FeType *check_identifier(FeCheckerState *s, FeNode *n, int read) { FeSym *sym; + (void)read; /* TRIAL PATCH: silence W303 under the check_m7.c wrapper */ sym = find_symbol(s->scope, n->text ? n->text : ""); if (!sym) { FeType *named=fe_type_intern(&s->c->types,n->text ? n->text : ""); diff --git a/fec/src/check_m7.c b/fec/src/check_m7.c index 193ecbf..9cfc506 100644 --- a/fec/src/check_m7.c +++ b/fec/src/check_m7.c @@ -98,6 +98,7 @@ static FeType *m7_check_expected(FeCheckerState *s, FeNode *value, return actual; } +/* TRIAL PATCH: m7_error_same was dead (W202); removed for this run. static int m7_error_same(FeCheckerState *s, FeType *a, FeType *b) { FeType *ea; @@ -108,6 +109,7 @@ static int m7_error_same(FeCheckerState *s, FeType *a, FeType *b) eb=fe_m7_error_type(&s->c->types,b); return ea && eb && fe_type_equal(ea,eb); } +*/ static FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) { diff --git a/fec/src/emit_c_m7.c b/fec/src/emitcm7.c similarity index 100% rename from fec/src/emit_c_m7.c rename to fec/src/emitcm7.c diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index 74d129a..e967920 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -17,6 +17,7 @@ M3 = "TESTS\\M3" M4 = "TESTS\\M4" M5 = "TESTS\\M5" M6 = "TESTS\\M6" +M7 = "TESTS\\M7" OUT = "OUT" # Emitted-C basenames that were hand-shortened for DOS 8.3. Keyed by milestone @@ -193,6 +194,17 @@ CASES: list[Case] = [ "okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", "okstatic", "oktemp", "oktrim", "okwcall")], + + # -- M7: optionals and error unions --------------------------------------- + *[_case(7, name, f"FEC.EXE --check {_fe(M7, name)}", False) for name in ( + "badcatch", "baddef", "baddir", "badercod", "badernam", "badetype", + "badnull", "badoref", "badorel", "badproj", "badqmark", "badret", + "badsome", "badtry", "badzero")], + *[_case(7, name, _emit(_fe(M7, name), f"{OUT}\\{name.upper()}.C", + output_first=True)) for name in ( + "okcatch", "okcatmov", "okcvoid", "okdeflt", "okiflet", "okmatch", + "oknull", "okorelse", "okpatvw", "okproj", "okrepl", "oktrdef", + "oktry")], ] MAX_MILESTONE: int = max(case.milestone for case in CASES) From 86bff9a06d8e97725cc308a9fbf74f2779f29bbc Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 23:09:30 +0900 Subject: [PATCH 092/184] dev: capture compiler diagnostics under DOS Every fec error message was written to stderr, and COMMAND.COM can only redirect handle 1 -- ">" is the whole vocabulary, "2>" is not parsed at all. So a failing compile recorded exit code 1 and a zero-byte log, and the actual message went to a screen nobody reads. Confirmed directly: `FEC.EXE --check` on a fixture that must fail produced rc=1 and 0 bytes of stdout. That is why an unexpected compiler failure was undiagnosable. It also means the M6 reject cases have only ever asserted "exit code was nonzero" -- the error text they nominally check has never been observable to the runner. Add fe_diag_stream(), which resolves once to stdout when FE_DIAG_STDOUT is set and stderr otherwise, and route diag.c and driver.c through it. The default is unchanged, so interactive use keeps writing to stderr; the runner sets the variable in RUN.BAT. The stderr references in check.c and emit_c.c are the Ferro language's own std.io.stderr writer and are deliberately untouched. Verified in DOSBox-X: 155 passed. A rejecting compile now records its message, source excerpt and caret in RESULTS\.LOG. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- fec/src/diag.c | 37 +++++++++++++++++++++++-------------- fec/src/diag.h | 7 +++++++ fec/src/driver.c | 12 ++++++------ src/ferrolang_vm/dosboxx.py | 3 +++ 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/fec/src/diag.c b/fec/src/diag.c index c9d35b1..d0df0f4 100644 --- a/fec/src/diag.c +++ b/fec/src/diag.c @@ -1,5 +1,14 @@ #include "diag.h" +#include + +FILE *fe_diag_stream(void) +{ + static FILE *stream=0; + if(!stream) stream=getenv("FE_DIAG_STDOUT") ? stdout : stderr; + return stream; +} + static unsigned long digits(unsigned long n) { unsigned long count=1; @@ -27,21 +36,21 @@ static void excerpt(const FeDiags *d, FeLoc loc) end=start; while(endstart) fwrite(src+start,1,(size_t)(end-start),stderr); - fputc('\n',stderr); - fputs(" ",stderr); - for(i=0;istart) fwrite(src+start,1,(size_t)(end-start),fe_diag_stream()); + fputc('\n',fe_diag_stream()); + fputs(" ",fe_diag_stream()); + for(i=0;ierrors++; - fprintf(stderr, "%s:%lu:%lu: error: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); + fprintf(fe_diag_stream(), "%s:%lu:%lu: error: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); excerpt(d,loc); } void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg) { d->errors++; - fprintf(stderr, "%s:%lu:%lu: error: ", loc.file ? loc.file : "", loc.line, loc.col); - fprintf(stderr, msg, arg); - fputc('\n', stderr); + fprintf(fe_diag_stream(), "%s:%lu:%lu: error: ", loc.file ? loc.file : "", loc.line, loc.col); + fprintf(fe_diag_stream(), msg, arg); + fputc('\n', fe_diag_stream()); excerpt(d,loc); } void fe_diag_note(FeLoc loc, const char *msg) { - fprintf(stderr, "%s:%lu:%lu: note: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); + fprintf(fe_diag_stream(), "%s:%lu:%lu: note: %s\n", loc.file ? loc.file : "", loc.line, loc.col, msg); } void fe_diag_note_src(FeDiags *d, FeLoc loc, const char *msg) diff --git a/fec/src/diag.h b/fec/src/diag.h index 300f90f..df27869 100644 --- a/fec/src/diag.h +++ b/fec/src/diag.h @@ -16,6 +16,13 @@ typedef struct FeDiags { unsigned long source_len; } FeDiags; +/* Stream diagnostics are written to. Defaults to stderr; returns stdout when + FE_DIAG_STDOUT is set in the environment. DOS offers no way to redirect + handle 2 -- COMMAND.COM understands ">" and nothing else -- so under the test + runner every error message would otherwise be written straight to the screen + and lost. Interactive use is unaffected: both streams reach the console. */ +FILE *fe_diag_stream(void); + void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len); void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg); void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg); diff --git a/fec/src/driver.c b/fec/src/driver.c index 3c3fc20..90903d5 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -8,7 +8,7 @@ static char *read_file(const char *name, unsigned long *size) { FILE *f; long n; char *p; - f=fopen(name,"rb"); if(!f){fprintf(stderr,"fec: cannot open %s\n",name);return 0;} + f=fopen(name,"rb"); if(!f){fprintf(fe_diag_stream(),"fec: cannot open %s\n",name);return 0;} if(fseek(f,0L,SEEK_END)!=0){fclose(f);return 0;} n=ftell(f); if(n<0){fclose(f);return 0;} rewind(f); p=(char *)malloc((unsigned long)n+1); if(!p){fclose(f);return 0;} if(n && fread(p,1,(size_t)n,f)!=(size_t)n){free(p);fclose(f);return 0;} fclose(f);p[n]='\0';*size=(unsigned long)n;return p; @@ -54,7 +54,7 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"--dump-tokens")==0) dump_tok=1; else if(strcmp(argv[i],"--check")==0) check_only=1; else if(strcmp(argv[i],"--emit-c")==0) emit=1; - else if(strcmp(argv[i],"-o")==0){if(i+1>=argc){fprintf(stderr,"fec: -o needs a path\n");return 2;}outname=argv[++i];} + else if(strcmp(argv[i],"-o")==0){if(i+1>=argc){fprintf(fe_diag_stream(),"fec: -o needs a path\n");return 2;}outname=argv[++i];} else if(strncmp(argv[i],"-o",2)==0 && argv[i][2]) outname=argv[i]+2; else if(strncmp(argv[i],"--target=bits16",15)==0) pointer_bits=16; else if(strncmp(argv[i],"--target=bits32",15)==0) pointer_bits=32; @@ -62,13 +62,13 @@ int main(int argc, char **argv) else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} - else {fprintf(stderr,"fec: unknown option %s\n",argv[i]);return 2;} + else {fprintf(fe_diag_stream(),"fec: unknown option %s\n",argv[i]);return 2;} } if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)+(emit?1:0)>1){ - fprintf(stderr,"fec: choose only one output mode\n"); + fprintf(fe_diag_stream(),"fec: choose only one output mode\n"); return 2; } - if(!file){fprintf(stderr,"fec: no input file\n");return 2;} + if(!file){fprintf(fe_diag_stream(),"fec: no input file\n");return 2;} src=read_file(file,&n); if(!src)return 2; fe_diags_init(&d,src,n); @@ -98,7 +98,7 @@ int main(int argc, char **argv) return 0; } out=outname?fopen(outname,"w"):stdout; - if(!out){fprintf(stderr,"fec: cannot create %s\n",outname);fe_ast_destroy(&ast);free(src);return 2;} + if(!out){fprintf(fe_diag_stream(),"fec: cannot create %s\n",outname);fe_ast_destroy(&ast);free(src);return 2;} fe_emit_c_init(&emitter,out,&check,pointer_bits,no_checks); fe_emit_c_program(&emitter); if(outname)fclose(out); diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index ac94cfd..c968663 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -159,6 +159,9 @@ def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", "set WATCOM=W:", "set INCLUDE=W:\\H", "set LIB=W:\\LIB286\\DOS;W:\\LIB286;W:\\LIB386\\DOS;W:\\LIB386", + # COMMAND.COM can only redirect handle 1, so fec diagnostics written to + # stderr never reach RESULTS\.LOG. Ask it for stdout instead. + "set FE_DIAG_STDOUT=1", build, "if not exist BUILD.OK goto BUILDFAIL", "echo PASS>RESULTS\\BUILD.RES", ] From 0a434d8a9a059ba3d9e0ff108837fd6bddc39e13 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 23:23:47 +0900 Subject: [PATCH 093/184] spec: record why the try rule cannot be enforced yet SPEC.md allows `try` only inside a function returning an error union, but check.c only tests it in the FE_N_EXPR_STMT case, so `var x = try e;` and `x = try e;` walk straight past. m5/owned.fe and m5/runtime.fe both depend on that gap. Moving the check onto the try expression is a four-line change and it is correct, but it cannot land yet. runtime.fe's `run` allocates and returns a value, so closing the hole forces it to return an error union -- and master rejects `return ;` in `-> !i32` ("return type mismatch") as well as a bare `return;` in `-> !void` ("void expression returned from value function"). Both need contextual success construction, which is M7 work. `catch` and `@trap()`, the two spellings SPEC offers as alternatives, are also M7-only, so there is no way to express `run` legally on master today. All three paths were tried in DOSBox-X, not assumed. Fix owned.fe now, since `main() -> !void` is legal today and matches m4/try-fpr.fe, and leave the checker alone until M7 lands with the rest. Verified: 155 passed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01BScg8CF1sAAM2zVHAu5zvW --- SPEC.AUDIT.md | 16 ++++++++++++++++ fec/tests/m5/owned.fe | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index cb1c787..83db644 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -557,3 +557,19 @@ M6~M9 구현 전에 함수-local 소유권 분석, optional/error 의미, 계층 - 근거: collision-free backend linkage와 M12 fixpoint를 동시에 보장한다. - 구현 영향: 정확한 escaping 문자는 구현 세부지만 deterministic separator encoding과 collision 검사가 필요하며 absolute path/address를 symbol에 포함할 수 없다. + +### `try` enforcement is blocked on M7 contextual construction + +- 문제: `SPEC.md`는 `try`를 에러 유니온 반환 함수 안에서만 허용하는데, `check.c`의 검사가 + `FE_N_EXPR_STMT`에만 걸려 있어 `var x = try e;`와 `x = try e;`를 통과시킨다. + `fec/tests/m5/runtime.fe`의 `run`과 `owned.fe`의 `main`이 이 구멍에 의존하고 있었다. +- 결정: 구멍은 M7과 함께 닫는다. M7 이전 master에서는 닫을 수 없다. +- 근거: 검사를 `try` 표현식으로 옮기면 `run`이 에러 유니온을 반환해야 하는데, master는 + `-> !i32`에서 `return ;`도, `-> !void`에서 명시적 `return;`도 거부한다 + ("return type mismatch" / "void expression returned from value function"). 둘 다 + contextual success construction이 필요하고 그것은 M7 작업이다. `catch`와 `@trap`도 + master에는 없어서 우회로가 없다. 실측으로 세 경로를 모두 확인했다. +- 구현 영향: M7 병합 시 `check.c`의 검사를 `check_expr`의 `try` 분기로 옮기고 + `FE_N_EXPR_STMT`의 중복 검사를 제거한다. `runtime.fe`의 `run`은 그때 에러 유니온 + 반환으로 바꾸고 `runtime.c`의 `extern long fe_m5_runtime_run(long)`을 함께 고친다. + `owned.fe`의 `main`은 M7 없이도 합법인 `-> !void`로 먼저 고쳐 두었다. diff --git a/fec/tests/m5/owned.fe b/fec/tests/m5/owned.fe index e3f44f6..3903ba0 100644 --- a/fec/tests/m5/owned.fe +++ b/fec/tests/m5/owned.fe @@ -1,6 +1,6 @@ unit m5_owned; -fn main() -> void { +fn main() -> !void { var p: ^i32 = try mem.create(0); p = try mem.create(0); p.^ = 7; From 05ae2bea9aec252edfbbad6c7de52472fcea7c1b Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 23:39:05 +0900 Subject: [PATCH 094/184] fix: unblock the DOS build for M7 sources --- fec/src/check.c | 5 ++--- fec/src/check_m7.c | 15 +-------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index 1258857..52a1999 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -808,10 +808,9 @@ static FeType *check_index(FeCheckerState *s, FeNode *n) n->sem_type=base->elem; return n->sem_type; } -static FeType *check_identifier(FeCheckerState *s, FeNode *n, int read) +static FeType *check_identifier(FeCheckerState *s, FeNode *n) { FeSym *sym; - (void)read; /* TRIAL PATCH: silence W303 under the check_m7.c wrapper */ sym = find_symbol(s->scope, n->text ? n->text : ""); if (!sym) { FeType *named=fe_type_intern(&s->c->types,n->text ? n->text : ""); @@ -843,7 +842,7 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) const char *op; if (!n) return unknown(c); if (n->kind == FE_N_IDENT) - return check_identifier(s, n, 1); + return check_identifier(s, n); if (n->kind == FE_N_LITERAL) { if (!n->text) return unknown(c); if (strcmp(n->text, "true") == 0 || strcmp(n->text, "false") == 0) diff --git a/fec/src/check_m7.c b/fec/src/check_m7.c index 9cfc506..34b0388 100644 --- a/fec/src/check_m7.c +++ b/fec/src/check_m7.c @@ -98,19 +98,6 @@ static FeType *m7_check_expected(FeCheckerState *s, FeNode *value, return actual; } -/* TRIAL PATCH: m7_error_same was dead (W202); removed for this run. -static int m7_error_same(FeCheckerState *s, FeType *a, FeType *b) -{ - FeType *ea; - FeType *eb; - if (!a || !b || a->kind!=FE_TYPE_ERROR_UNION || - b->kind!=FE_TYPE_ERROR_UNION) return 0; - ea=fe_m7_error_type(&s->c->types,a); - eb=fe_m7_error_type(&s->c->types,b); - return ea && eb && fe_type_equal(ea,eb); -} -*/ - static FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) { FeFieldType *field; @@ -433,7 +420,7 @@ static FeType *m7_check_expr(FeCheckerState *s, FeNode *n) return n->sem_type; } if (n->kind==FE_N_IDENT) - return check_identifier(s,n,1); + return check_identifier(s,n); if (n->kind==FE_N_LITERAL) return check_expr(s,n); if (n->kind==FE_N_CALL) From 44dc5562eff90da91e332b2dda88a37958fc979c Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Sun, 16 Aug 2026 23:48:18 +0900 Subject: [PATCH 095/184] fix: repair three M7 C emission defects emitcm7.c reimplements the emitter for sources that mention M7 syntax, and three things were lost in the port. All of them only reached M4 fixtures, because M1-M3 and M6 take the M6 fast path. Local initializers named "0". The LET/VAR case passed the declaration node to emit_lvalue, which matches only IDENT/MEMBER/INDEX and otherwise falls through to the raw expression path -- a declaration node renders there as "0", so every `var x = init;` emitted `0 = init;`. Teach emit_lvalue that a declaration names its own storage, which also fixes the catch path that had the same call. Aggregate initializers were not constant. A string-literal `const` lowered to a maker call, but C89 requires a constant expression for aggregate initializers at file scope and for automatics alike, and the build runs with -za. Restore the braced form for both the local and the global path. Slice helpers were never emitted. The final loop in m7_emit_type_helpers is commented as reusing the M3 index/slice generator but only ported the index half, so bodies called fe_slice_*/fe_full_*/fe_tail_* that no declaration defined. Emit the three slicers for array and slice types. The first defect masked the other two: wcc386 died on `0 = ...` before it could reach them, and wcl386 reports that as "Unable to invoke wcc386.exe" with no diagnostic, which is why this needed bisecting against master's output rather than reading an error message. M1-M7: 7 failed, 176 passed -> 3 failed, 180 passed. The remainder is m5 runtime, which is a separate fixture issue. --- fec/src/emitcm7.c | 58 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/fec/src/emitcm7.c b/fec/src/emitcm7.c index ad971b1..7507d1d 100644 --- a/fec/src/emitcm7.c +++ b/fec/src/emitcm7.c @@ -294,6 +294,7 @@ static void m7_emit_drop_helpers(FeEmitter *e) static void m7_emit_type_helpers(FeEmitter *e) { FeType *t; + FeType *st; unsigned i; unsigned j; const char *ct; @@ -396,11 +397,34 @@ static void m7_emit_type_helpers(FeEmitter *e) if (!e->no_checks) fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); fputs("return x.a[i]; }\n",e->out); + if (t->slicer) { + st=fe_type_slice(&e->check->types,t->elem); + fprintf(e->out,"static %s %s(%s *x, unsigned long a, unsigned long b) { ", + m7_c_type(e,st),t->slicer,m7_c_type(e,t)); + if (!e->no_checks) + fprintf(e->out,"if (a > b || b > %lu) fe_trap_bounds(); ",t->length); + fprintf(e->out,"return %s(x->a+a,b-a); }\n",st->maker); + fprintf(e->out,"static %s %s(%s *x) { return %s(x,0,%lu); }\n", + m7_c_type(e,st),t->full_slicer,m7_c_type(e,t),t->slicer,t->length); + fprintf(e->out,"static %s %s(%s *x, unsigned long a) { return %s(x,a,%lu); }\n", + m7_c_type(e,st),t->tail_slicer,m7_c_type(e,t),t->slicer,t->length); + } } else if (t->kind==FE_TYPE_SLICE && t->indexer) { fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); if (!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); fputs("return x.p[i]; }\n",e->out); + if (t->slicer) { + fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", + m7_c_type(e,t),t->slicer,m7_c_type(e,t)); + if (!e->no_checks) + fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); + fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker); + fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n", + m7_c_type(e,t),t->full_slicer,m7_c_type(e,t),t->slicer); + fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n", + m7_c_type(e,t),t->tail_slicer,m7_c_type(e,t),t->slicer); + } } } } @@ -495,6 +519,14 @@ static void emit_lvalue(FeEmitter *e, FeNode *n) fputs(cname(n,"fe_local"),e->out); return; } + /* A declaration names its own storage. The initializer for `let`/`var` is + emitted as a separate assignment statement, so the declaration node is + handed here as the target; without this it falls through to the raw + expression path, which emits a declaration as "0" and produces `0 = ...`. */ + if (n->kind==FE_N_LET || n->kind==FE_N_VAR || n->kind==FE_N_CONST) { + fputs(cname(n,"fe_local"),e->out); + return; + } if (n->kind==FE_N_MEMBER) { bt=n->a ? n->a->sem_type : 0; if (n->text && strcmp(n->text,".?")==0) { @@ -762,12 +794,31 @@ static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) (void)x; } +/* Emit the initializer for a `const` declaration. + + A string literal normally lowers to a maker call, but C89 requires the + initializer of an aggregate -- at file scope and for automatics alike -- to be + a constant expression, and the build runs with -za. Emit the slice braced + instead. Returns non-zero when it handled the initializer. */ +static int m7_emit_const_init(FeEmitter *e, FeNode *n) +{ + if (n->kind!=FE_N_CONST || !n->b || n->b->kind!=FE_N_LITERAL || + !n->b->text || n->b->text[0]!='"') return 0; + fputs("{ (const unsigned char*)",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(", sizeof(",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(")-1 }",e->out); + return 1; +} + static void emit_decl(FeEmitter *e, FeNode *n) { pad(e); fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); fputs(cname(n,"fe_local"),e->out); if (n->kind==FE_N_CONST && n->b) { - fputs(" = ",e->out); emit_expr(e,n->b); + fputs(" = ",e->out); + if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); } fputs(";\n",e->out); if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && @@ -1267,7 +1318,10 @@ void fe_emit_c_program(FeEmitter *e) if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); fputs(cname(n,"fe_global"),e->out); - if (n->b) { fputs(" = ",e->out); emit_expr(e,n->b); } + if (n->b) { + fputs(" = ",e->out); + if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); + } fputs(";\n",e->out); } } From 6713a934f5c7c11d82cf53176f035a193dff5091 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 00:01:51 +0900 Subject: [PATCH 096/184] fix: emit mem.create and mem.alloc_slice on the M7 path m7_emit_call reimplements call emission and only carried over mem.destroy and mem.replace, so every mem.create/mem.alloc_slice fell through to the generic member path and emitted `fe_missing.create(0)`. wcc386 does not diagnose that -- it terminates with exit 255, which wcl386 reports as "Unable to invoke wcc386.exe" with no message at all. The breakage covered all 17 allocation sites in m5/runtime.fe, and it was invisible because m5-owned and m5-defer only emit C; m5-runtime is the one M5 fixture that compiles and links what was generated. Also make runtime.fe legal: `run` used `try` while returning i32, which SPEC allows only in a function returning an error union. It is `-> !i32` now, which M7 accepts because contextual success construction lands with it, and runtime.c takes the { error, value } struct the error union lowers to. Found by calling wcc386 directly instead of through wcl386, which is the only way to see a compiler crash here. M1-M7: 183 passed. --- fec/src/emitcm7.c | 22 ++++++++++++++++++++++ fec/tests/m5/runtime.c | 11 +++++++---- fec/tests/m5/runtime.fe | 2 +- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/fec/src/emitcm7.c b/fec/src/emitcm7.c index 7507d1d..27ca387 100644 --- a/fec/src/emitcm7.c +++ b/fec/src/emitcm7.c @@ -595,6 +595,28 @@ static void m7_emit_call(FeEmitter *e, FeNode *n) emit_expr(e,n->children->next); fputc(')',e->out); return; } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"create")==0 && n->children) { + FeType *created=n->children->sem_type; + FeType *owned=fe_type_owned(&e->check->types,created); + FeType *result=fe_type_error_union(&e->check->types,owned); + fputs(result->alloc_cname ? result->alloc_cname : "fe_bad_alloc",e->out); + fputc('(',e->out); emit_expr(e,n->children); fputc(')',e->out); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"alloc_slice")==0 && n->children && + n->children->next) { + FeType *result=n->sem_type; + fputs(result && result->alloc_cname ? result->alloc_cname : + "fe_bad_slice_alloc",e->out); + fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out); + return; + } if (n->text && (strcmp(n->text,"@print")==0 || strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { emit_m4_builtin(e,n); diff --git a/fec/tests/m5/runtime.c b/fec/tests/m5/runtime.c index 43e51e0..5d3e6bb 100644 --- a/fec/tests/m5/runtime.c +++ b/fec/tests/m5/runtime.c @@ -7,7 +7,9 @@ extern void *malloc(size_t size); extern void free(void *p); -extern long fe_m5_runtime_run(long mode); +/* `run` returns !i32, which lowers to { error, value }. */ +struct fe_result_value_9 { unsigned short e; long v; }; +extern struct fe_result_value_9 fe_m5_runtime_run(long mode); extern unsigned short fe_m5_runtime_conditional(unsigned char flag); extern unsigned short fe_m5_runtime_argument_cleanup(void); extern unsigned short fe_m5_runtime_owned_slice(unsigned long n); @@ -64,9 +66,10 @@ void m5_free(void *p) int main(void) { - if (fe_m5_runtime_run(0) != 0) return 1; - if (fe_m5_runtime_run(1) != 9) return 2; - if (fe_m5_runtime_run(2) != 0) return 3; + struct fe_result_value_9 r; + r = fe_m5_runtime_run(0); if (r.e != 0 || r.v != 0) return 1; + r = fe_m5_runtime_run(1); if (r.e != 0 || r.v != 9) return 2; + r = fe_m5_runtime_run(2); if (r.e != 0 || r.v != 0) return 3; if (fe_m5_runtime_conditional(0) != 0) return 4; if (fe_m5_runtime_conditional(1) != 0) return 5; if (fe_m5_runtime_argument_cleanup() != 0) return 6; diff --git a/fec/tests/m5/runtime.fe b/fec/tests/m5/runtime.fe index 38a35da..b9dab0c 100644 --- a/fec/tests/m5/runtime.fe +++ b/fec/tests/m5/runtime.fe @@ -2,7 +2,7 @@ unit m5_runtime; fn take(p: ^i32) -> void { mem.destroy(p); } -pub fn run(mode: i32) -> i32 { +pub fn run(mode: i32) -> !i32 { var p: ^i32 = try mem.create(0); defer { mem.destroy(p); } p.^ = 7; From a2428ca5da64e19a63fba6eabf5508f997a3739c Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 00:03:26 +0900 Subject: [PATCH 097/184] fix: enforce the try rule at every position SPEC.md allows `try` only inside a function returning an error union, but the check sat in the FE_N_EXPR_STMT case, so it only ever saw a bare `try e;` and walked past `var x = try e;` and `x = try e;`. Move it onto the try expression in check_expr and drop the statement-level copy. This could not land before M7: closing the hole forces m5/runtime.fe's `run` to return an error union, and value returns from `-> !T` need contextual success construction. That arrives with M7, and `run` is now `-> !i32`, so the rule can be enforced. Supersedes the SPEC.AUDIT.md entry that recorded the blockage. M1-M7: 183 passed. --- fec/src/check.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index 52a1999..415ffcb 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -871,6 +871,12 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) if (known(a) && !fe_type_is_integer(a)) err(c, n->loc, "unary '-' requires integer"); } else if (strcmp(op, "try") == 0) { + /* SPEC 6.4: try is only allowed inside a function returning an error + union. Checked on the expression rather than on the statement so + that it also covers `var x = try e;` and `x = try e;`, which the + statement-level check walked straight past. */ + if (!s->ret || s->ret->kind != FE_TYPE_ERROR_UNION) + err(c,n->loc,"try requires an enclosing error result"); if (a && a->kind==FE_TYPE_ERROR_UNION) a=a->error_value; else { @@ -1549,11 +1555,9 @@ static void check_stmt(FeCheckerState *s, FeNode *n) } break; case FE_N_EXPR_STMT: + /* The enclosing-error-result check lives on the try expression itself, + so a bare `try e;` needs nothing extra here. */ check_expr(s, n->a); - if (n->a && n->a->kind==FE_N_UNARY && n->a->text && - strcmp(n->a->text,"try")==0 && - (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION)) - err(c,n->loc,"try requires an enclosing error result"); break; case FE_N_DEFER: ++s->defer_depth; From 23079ba9fbaa9da0ca22198812fb67e9b94c1ffe Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 00:03:26 +0900 Subject: [PATCH 098/184] spec: mark the try enforcement entry resolved by M7 --- SPEC.AUDIT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md index 83db644..2cf1441 100644 --- a/SPEC.AUDIT.md +++ b/SPEC.AUDIT.md @@ -563,7 +563,7 @@ M6~M9 구현 전에 함수-local 소유권 분석, optional/error 의미, 계층 - 문제: `SPEC.md`는 `try`를 에러 유니온 반환 함수 안에서만 허용하는데, `check.c`의 검사가 `FE_N_EXPR_STMT`에만 걸려 있어 `var x = try e;`와 `x = try e;`를 통과시킨다. `fec/tests/m5/runtime.fe`의 `run`과 `owned.fe`의 `main`이 이 구멍에 의존하고 있었다. -- 결정: 구멍은 M7과 함께 닫는다. M7 이전 master에서는 닫을 수 없다. +- 결정: 구멍은 M7과 함께 닫는다. M7 이전 master에서는 닫을 수 없다. (M7에서 해소됨) - 근거: 검사를 `try` 표현식으로 옮기면 `run`이 에러 유니온을 반환해야 하는데, master는 `-> !i32`에서 `return ;`도, `-> !void`에서 명시적 `return;`도 거부한다 ("return type mismatch" / "void expression returned from value function"). 둘 다 From aaf31302d5ef56b295a09161afd479a4c2b92c02 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:14:47 +0900 Subject: [PATCH 099/184] dev: compile the generated C, and fix what that caught Thirty-one cases only ever emitted C and never built it -- m5 owned/defer, and every ok* acceptance fixture in m6 and m7. They asserted that fec produced a file, not that the file was a program, so a malformed emission sat unnoticed. Compile each one with wcl386 -c. Fixtures without a main cannot be run, so this is the floor for them; it is a conformance check on the backend's output, not an assertion about C, so a future backend swaps the command rather than the intent. It found two defects in the M6 emitter immediately, both predating M7: &s[0] took the address of an rvalue. Borrowing went through emit_expr, which lowers an index to the bounds-checking accessor, so `&s[0]` became `&fe_idx_slice_type_2(s, 0)`. emit_lvalue spells the same element as `s.p[0]`. The M7 emitter already did this correctly; only the M6 path was wrong. line.trim() was never lowered. SPEC lists trim among the built-in alias methods on str and the checker accepts it, but no emitter case existed, so it emitted `fe_l_line_0.trim()` -- a member access on a slice struct. Emit the helper, and only for programs that actually trim. Also set core=dynamic and cycles=max. core=auto uses the interpreter in real mode, which is where the 16-bit compiler build spends its time; nothing here is timing sensitive. m6 drops from 31s to 17s. Raise the DOSBox timeout to match a whole-suite run, which now pays a process spawn per compile check. Verified: --only m6, 57 passed. --- fec/src/emit_c.c | 40 +++++++++++++++++++++++++++++- src/ferrolang_vm/dosboxx.py | 13 ++++++++-- src/ferrolang_vm/registry.py | 48 ++++++++++++++++++++++++++++++------ 3 files changed, 90 insertions(+), 11 deletions(-) diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 0a053dc..0e919a1 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -199,6 +199,22 @@ static void emit_drop_helpers(FeEmitter *e) } } +/* SPEC 12.3 lists `trim` among the built-in alias methods on `str`, called as + `line.trim()`. The checker accepts it; this emits the lowering. Only the + helper for slice types actually reached by a trim call is emitted, so a + program that never trims does not carry it. */ +static int node_uses_trim(FeNode *n) +{ + FeNode *x; + if (!n) return 0; + if (n->kind==FE_N_CALL && n->a && n->a->kind==FE_N_MEMBER && + n->a->b && n->a->b->text && strcmp(n->a->b->text,"trim")==0) return 1; + if (node_uses_trim(n->a) || node_uses_trim(n->b) || node_uses_trim(n->c)) + return 1; + for (x=n->children; x; x=x->next) if (node_uses_trim(x)) return 1; + return 0; +} + static void emit_type_helpers(FeEmitter *e) { FeType *t; @@ -278,6 +294,14 @@ static void emit_type_helpers(FeEmitter *e) fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker); fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n",t->cname,t->full_slicer,t->cname,t->slicer); fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n",t->cname,t->tail_slicer,t->cname,t->slicer); + if (node_uses_trim(e->check->ast->root) && !t->ref_mut) { + fprintf(e->out, + "static %s fe_trim_%s(%s s) { unsigned long a=0; unsigned long b=s.n;" + " while (aa && (s.p[b-1]==' '||s.p[b-1]=='\\t'||s.p[b-1]=='\\r'||s.p[b-1]=='\\n')) --b;" + " return %s(s.p+a,b-a); }\n", + t->cname,t->cname,t->cname,t->maker); + } } } } @@ -812,7 +836,12 @@ static void emit_expr(FeEmitter *e, FeNode *n) fputc('(', e->out); fputs(strcmp(op,"&mut")==0 ? "&" : op, e->out); } - emit_expr(e, n->a); + /* Borrowing needs a place, not a value. emit_expr lowers an index to + the bounds-checking accessor, and the address of that call is not an + lvalue -- `&s[0]` became `&fe_idx_slice_type_2(s, 0)`, which C + rejects. emit_lvalue spells the same element as `s.p[0]`. */ + if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) emit_lvalue(e, n->a); + else emit_expr(e, n->a); fputc(')', e->out); break; case FE_N_BINARY: @@ -871,6 +900,15 @@ static void emit_expr(FeEmitter *e, FeNode *n) fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out); special=1; } + else if(n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"trim")==0 && !n->children && + n->a->a && n->a->a->sem_type && + n->a->a->sem_type->kind==FE_TYPE_SLICE && + n->a->a->sem_type->cname) { + fputs("fe_trim_",e->out); fputs(n->a->a->sem_type->cname,e->out); + fputc('(',e->out); emit_expr(e,n->a->a); fputc(')',e->out); + special=1; + } else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"mem")==0 && n->a->b && diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index c968663..1a30e0f 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -262,7 +262,13 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, console = run_root / "CONSOLE.LOG" config = run_root / "DOSBOX.CON" config.write_text( - f"[log]\nlogfile={console}\n[dosbox]\nlog console=quiet\n", + # core=auto falls back to the interpreter in real mode, which is + # where the 16-bit compiler build spends its time. Nothing here is + # timing sensitive -- it is a compiler and a batch file -- so ask for + # the recompiler and uncapped cycles explicitly. + f"[log]\nlogfile={console}\n" + f"[dosbox]\nlog console=quiet\n" + f"[cpu]\ncore=dynamic\ncycles=max\n", encoding="ascii", ) (fec / "RUN.BAT").write_text( @@ -279,7 +285,10 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, "-c", f'mount W "{watcom}" -ro', "-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT", ]) - completed = subprocess.run(command, check=False, timeout=300) + # Every case pays a DOS process spawn, and the compile-only checks spawn + # wcc386 once each, so the whole-suite run is minutes rather than the + # under-a-minute a single milestone takes. + completed = subprocess.run(command, check=False, timeout=1800) if completed.returncode != 0: raise DosboxError(f"DOSBox-X exited with status {completed.returncode}") if not (fec / "RUN.OK").is_file(): diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py index e967920..5a47497 100644 --- a/src/ferrolang_vm/registry.py +++ b/src/ferrolang_vm/registry.py @@ -74,6 +74,41 @@ def _wcl(exe: str, *sources: str, bits: int = 32, strict: bool = False, return " ".join(parts) +def _wcc(source: str, obj: str) -> str: + """Compile the generated C without linking. + + A fixture with no ``main`` cannot be run, so this is the floor for it: the + backend's output has to survive the compiler the project actually ships + with. It catches a malformed emission -- an unnamed assignment target, a + helper that is called but never defined, an initializer C89 rejects -- which + otherwise sits unnoticed in a case that only ever emitted text. + + This asserts nothing about the C itself; it is a conformance check on the + backend's output, so a future non-C backend swaps the command rather than + the intent. Never grep the generated C to prove a language feature -- write a + fixture whose exit code differs instead, as TESTS\\M3\\NOCHK.FE does. + """ + # Through the wcl386 driver with -c rather than calling wcc386 directly: + # wcc386 writes its diagnostics to stderr, which COMMAND.COM cannot + # redirect, so a failure would report a count and no messages. The driver + # leaves an .ERR file, which the runner already collects. + return f"WCL386 -q -za -wx -wcd=202 -bt=dos -c -fo={obj} {source}" + + +def _accepts(milestone: int, directory: str, names: tuple[str, ...], *, + output: str = OUT, output_first: bool = True) -> list[Case]: + """Fixtures that must compile: emit the C, then build it.""" + cases: list[Case] = [] + for name in names: + cfile = f"{output}\\{name.upper()}.C" + cases.append(_case(milestone, name, + _emit(_fe(directory, name), cfile, + output_first=output_first))) + cases.append(_case(milestone, f"{name}-cc", + _wcc(cfile, f"{OUT}\\{name.upper()}.OBJ"))) + return cases + + def _dump_ast(milestone: int, directory: str, names: tuple[str, ...], *, suffix: str, ok: bool = True, prefix: str = "") -> list[Case]: return [ @@ -171,8 +206,7 @@ CASES: list[Case] = [ "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls")), # -- M5: defer and ownership ---------------------------------------------- - _case(5, "defer", _emit(_fe(M5, "defer"), f"{M5}\\DEFER.C")), - _case(5, "owned", _emit(_fe(M5, "owned"), f"{M5}\\OWNED.C")), + *_accepts(5, M5, ("defer", "owned"), output=M5, output_first=False), *_rejects(5, M5, ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", "bad-proj", "bad-clos", "bad-loop")), # The runtime case links the generated C against a hand-written allocator @@ -189,22 +223,20 @@ CASES: list[Case] = [ "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", "badtwo", "badup", "badweak")], - *[_case(6, name, _emit(_fe(M6, name), f"{OUT}\\{name.upper()}.C", - output_first=True)) for name in ( + *_accepts(6, M6, ( "okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", - "okstatic", "oktemp", "oktrim", "okwcall")], + "okstatic", "oktemp", "oktrim", "okwcall")), # -- M7: optionals and error unions --------------------------------------- *[_case(7, name, f"FEC.EXE --check {_fe(M7, name)}", False) for name in ( "badcatch", "baddef", "baddir", "badercod", "badernam", "badetype", "badnull", "badoref", "badorel", "badproj", "badqmark", "badret", "badsome", "badtry", "badzero")], - *[_case(7, name, _emit(_fe(M7, name), f"{OUT}\\{name.upper()}.C", - output_first=True)) for name in ( + *_accepts(7, M7, ( "okcatch", "okcatmov", "okcvoid", "okdeflt", "okiflet", "okmatch", "oknull", "okorelse", "okpatvw", "okproj", "okrepl", "oktrdef", - "oktry")], + "oktry")), ] MAX_MILESTONE: int = max(case.milestone for case in CASES) From 044c0f0e98964a4b56c715c14bc0507a5ff4b0c4 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:17:40 +0900 Subject: [PATCH 100/184] dev: cache the compiler build and drop unused headers Every run rebuilt fec from scratch inside DOS, about 14 seconds, even when no source had changed. Key a cache on the hash of fec/src plus build-dos.bat and restore FEC.EXE when it matches; the toolchain itself is pinned by dosboxx.lock.json so it cannot drift under a hit. Only a passing build is cached, and the batch skips BUILD.BAT on a hit because it would delete and rebuild the executable it was just given. Generated C included stdio.h unconditionally, but only the M4 writer runtime reaches it. A 26-line unit was pulling in roughly 1900 lines of headers, paid once per compile check. Emit it only when the runtime is emitted. --only m6, 57 passed, over three changes: 31s before 17s core=dynamic 2s warm compiler cache Cold runs still pay the build once. --- fec/src/emit_c.c | 7 ++++++- fec/src/emitcm7.c | 5 ++++- src/ferrolang_vm/dosboxx.py | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 0e919a1..6d44e33 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -1487,7 +1487,12 @@ void fe_emit_c_program(FeEmitter *e) need_m4=node_uses_m4(e->check->ast->root); for (type=e->check->types.types; type; type=type->next) if (strcmp(type->name,"io.Writer")==0) need_m4=1; - fputs("/* generated by fec M4 */\n#include \n#include \n#include \n#include \ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out); + /* stdio is only reached by the M4 writer runtime (fwrite/stdout/stderr). + Parsing it costs far more than the generated body -- a 26-line unit pulls + in about 1900 lines of headers -- so leave it out when nothing uses it. */ + fputs("/* generated by fec M4 */\n#include \n#include \n#include \n", e->out); + if (need_m4) fputs("#include \n", e->out); + fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out); if (e->pointer_bits==16) fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); else diff --git a/fec/src/emitcm7.c b/fec/src/emitcm7.c index 27ca387..60d16cf 100644 --- a/fec/src/emitcm7.c +++ b/fec/src/emitcm7.c @@ -1327,7 +1327,10 @@ void fe_emit_c_program(FeEmitter *e) need_m4=node_uses_m4(e->check->ast->root); for (type=e->check->types.types;type;type=type->next) if (strcmp(type->name,"io.Writer")==0) need_m4=1; - fputs("/* generated by fec M7 */\n#include \n#include \n#include \n#include \ntypedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); + /* See emit_c.c: stdio only comes in with the M4 writer runtime. */ + fputs("/* generated by fec M7 */\n#include \n#include \n#include \n",e->out); + if (need_m4) fputs("#include \n",e->out); + fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); if (e->pointer_bits==16) fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); else diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index 1a30e0f..102b5b3 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -151,10 +151,14 @@ def _rc_batch() -> str: return "\r\n".join(lines) -def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool) -> str: +def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool, + prebuilt: bool = False) -> str: # The compiler build is the step that fails first and blocks everything after # it, so its output is captured exactly like a case command's. build = "call BUILD.BAT" if trace_dos else "call BUILD.BAT > RESULTS\\BUILD.LOG" + if prebuilt: + # FEC.EXE was restored from cache; BUILD.BAT would delete and rebuild it. + build = "echo OK>BUILD.OK" lines = [ "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", "set WATCOM=W:", "set INCLUDE=W:\\H", @@ -247,6 +251,23 @@ class SuiteRun: shutil.rmtree(self.root, ignore_errors=True) +def _compiler_key() -> str: + """Hash of everything the compiler build reads. + + Sources and the build batch only; the toolchain itself is pinned by + dosboxx.lock.json, so it cannot drift underneath a cache hit. + """ + digest = hashlib.sha256() + paths = sorted((ROOT / "fec" / "src").rglob("*")) + paths.append(ROOT / "fec" / "build-dos.bat") + for path in paths: + if not path.is_file(): + continue + digest.update(path.name.encode("utf-8")) + digest.update(path.read_bytes()) + return digest.hexdigest()[:16] + + def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, trace_dos: bool = False) -> SuiteRun: dosbox, watcom = resolve_tools() @@ -254,6 +275,7 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, run_root = Path(tempfile.mkdtemp(prefix="suite-", dir=RUNS)) result = SuiteRun(run_root, cases, keep) fec = result.fec + cached = CACHE / "compilers" / f"{_compiler_key()}.exe" try: shutil.copytree(ROOT / "fec" / "src", fec / "SRC") shutil.copytree(ROOT / "fec" / "std", fec / "STD") @@ -271,8 +293,11 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, f"[cpu]\ncore=dynamic\ncycles=max\n", encoding="ascii", ) + if cached.is_file(): + shutil.copy2(cached, fec / "FEC.EXE") (fec / "RUN.BAT").write_text( - _batch(cases, show_dos=show_dos, trace_dos=trace_dos), + _batch(cases, show_dos=show_dos, trace_dos=trace_dos, + prebuilt=cached.is_file()), encoding="ascii", newline="", ) (fec / "RC.BAT").write_text(_rc_batch(), encoding="ascii", newline="") @@ -293,6 +318,10 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, raise DosboxError(f"DOSBox-X exited with status {completed.returncode}") if not (fec / "RUN.OK").is_file(): raise DosboxError("DOSBox-X did not complete the test batch") + built = fec / "FEC.EXE" + if not cached.is_file() and built.is_file() and result.result() == "PASS": + cached.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(built, cached) return result except Exception: result.keep = True From 0132ad413557471600278f581ab2575a015e0e77 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:34:20 +0900 Subject: [PATCH 101/184] fix: lower short catch as statements, and drop core=dynamic Compiling okcatmov.fe's output crashed wcc386 hard enough to take DOSBox-X down with it -- "no byte handler for write to ffffffc", emulator exit 1, the whole suite lost rather than one case failing. `result catch fallback` in return position lowered to `((tmp = X), tmp.e ? fallback : tmp.v)`, and with a moved operand X is itself `(fe_live_x=0, x)`, so the compiler met a struct assignment whose right side was a comma expression. Lower it as statements, and clear a move flag on its own line rather than inside the assignment. Revert core=dynamic from the previous commit. It was about 5x faster and it is wrong: the recompiler loses abort()'s exit status, so a trapping program exits 0 and the M3 bounds cases stop reporting the trap they exist to prove. Checked against a compiler built under core=normal, so the fault is in running the generated program, not in building the compiler. Also fix the build cache: BUILD.BAT puts the Watcom binaries on PATH, and skipping it on a cache hit left the case commands without it, which silently changed how the trap programs terminated. Reproduce that line. Per milestone, warm cache: m1 2s, m2 5s, m3 20s, m4 7s, m5 6s, m6 14s, m7 11s. All green, 220 cases. --- fec/src/emitcm7.c | 46 +++++++++++++++++++++++++++++++++++++ src/ferrolang_vm/dosboxx.py | 17 ++++++++------ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/fec/src/emitcm7.c b/fec/src/emitcm7.c index 60d16cf..c91b96c 100644 --- a/fec/src/emitcm7.c +++ b/fec/src/emitcm7.c @@ -670,6 +670,26 @@ static void m7_emit_call(FeEmitter *e, FeNode *n) } } +/* `dst = ;` as a statement, avoiding a comma expression on the right. + + A consumed identifier lowers to `(fe_live_x=0, x)`. When dst is a struct, + Watcom crashes on a struct assignment whose right side is a comma expression + -- hard enough to take DOSBox-X down with it -- so clear the move flag as its + own statement and assign the plain name. */ +static void m7_emit_assign_stmt(FeEmitter *e, const char *dst, FeNode *src) +{ + if (src && src->kind==FE_N_IDENT && (src->flags & FE_OWN_NODE_CONSUMED) && + src->sem_type && type_needs_drop(src->sem_type)) { + pad(e); fputs("fe_live_",e->out); fputs(cname(src,"owned"),e->out); + fputs("=0;\n",e->out); + pad(e); fputs(dst,e->out); fputs(" = ",e->out); + fputs(cname(src,"fe_missing"),e->out); fputs(";\n",e->out); + return; + } + pad(e); fputs(dst,e->out); fputs(" = ",e->out); + emit_expr(e,src); fputs(";\n",e->out); +} + static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) { FeNode *x; @@ -1208,6 +1228,32 @@ static void emit_stmt(FeEmitter *e, FeNode *n) fputs(";\n",e->out); emit_cleanup_all(e); pad(e); fputs("return fe_return_value;\n",e->out); + } else if (n->a && n->a->kind==FE_N_BINARY && !n->a->c && + fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH && + e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + /* Short catch in return position. As an expression this lowers to + `((tmp = X), tmp.e ? fallback : tmp.v)`, and when X carries a move + it becomes a struct assignment whose right side is itself a comma + expression -- which crashes wcc386 hard enough to take DOSBox-X + down with it. The same lowering as statements is also plainer. */ + FeNode *cx=n->a; + FeType *res=cx->a ? cx->a->sem_type : 0; + int has_value=res && res->error_value && + res->error_value->kind!=FE_TYPE_VOID; + m7_emit_assign_stmt(e,cx->aux_cname,cx->a); + pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); + if (has_value) fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + pad(e); fputs("fe_return_value = ",e->out); + emit_expr(e,cx->b); fputs(";\n",e->out); + --e->indent; pad(e); fputs("} else {\n",e->out); ++e->indent; + pad(e); fputs("fe_return_value = ",e->out); + fputs(cx->aux_cname,e->out); + if (has_value) fputs(".v",e->out); + fputs(";\n",e->out); + --e->indent; pad(e); fputs("}\n",e->out); + emit_cleanup_all(e); + pad(e); fputs("return fe_return_value;\n",e->out); } else { if (n->a && e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { pad(e); fputs("fe_return_value = ",e->out); diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index 102b5b3..d6c8c75 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -158,7 +158,10 @@ def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool, build = "call BUILD.BAT" if trace_dos else "call BUILD.BAT > RESULTS\\BUILD.LOG" if prebuilt: # FEC.EXE was restored from cache; BUILD.BAT would delete and rebuild it. - build = "echo OK>BUILD.OK" + # It also puts the Watcom binaries on PATH, which the case commands need + # after it, so that line has to be reproduced rather than skipped. + build = ("set PATH=%WATCOM%\\BINW;%WATCOM%\\BINP;%PATH%\r\n" + "echo OK>BUILD.OK") lines = [ "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", "set WATCOM=W:", "set INCLUDE=W:\\H", @@ -284,13 +287,13 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, console = run_root / "CONSOLE.LOG" config = run_root / "DOSBOX.CON" config.write_text( - # core=auto falls back to the interpreter in real mode, which is - # where the 16-bit compiler build spends its time. Nothing here is - # timing sensitive -- it is a compiler and a batch file -- so ask for - # the recompiler and uncapped cycles explicitly. + # Do not tune [cpu] here. core=dynamic is roughly 5x faster but the + # recompiler loses abort()'s exit status -- a program that traps + # exits 0 instead, so the M3 bounds cases stop reporting the trap + # they exist to prove. Verified against a compiler built under + # core=normal, so it is the runtime and not the build. f"[log]\nlogfile={console}\n" - f"[dosbox]\nlog console=quiet\n" - f"[cpu]\ncore=dynamic\ncycles=max\n", + f"[dosbox]\nlog console=quiet\n", encoding="ascii", ) if cached.is_file(): From d5e7a8d8d63e8239ff9caac6de037c7f1175a8ab Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:36:48 +0900 Subject: [PATCH 102/184] dev: restore core=dynamic; the trap failures were the PATH bug The previous commit blamed core=dynamic for the M3 bounds cases exiting 0 instead of trapping. That was wrong. Both suspects were live at the time -- the recompiler and a cache hit that skipped BUILD.BAT's `set PATH` -- and the core was ruled out before the PATH line was restored, so the test proved nothing. Retested with the PATH fix in place: core=dynamic gives 50 passed on m3, the traps included. abort() reports its exit status fine under the recompiler. Per milestone, warm cache, all green: m1 2s m2 3s m3 7s m4 3s m5 3s m6 6s m7 5s 29s total against 65s on the interpreter. --- src/ferrolang_vm/dosboxx.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py index d6c8c75..0dcc2b3 100644 --- a/src/ferrolang_vm/dosboxx.py +++ b/src/ferrolang_vm/dosboxx.py @@ -287,13 +287,14 @@ def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, console = run_root / "CONSOLE.LOG" config = run_root / "DOSBOX.CON" config.write_text( - # Do not tune [cpu] here. core=dynamic is roughly 5x faster but the - # recompiler loses abort()'s exit status -- a program that traps - # exits 0 instead, so the M3 bounds cases stop reporting the trap - # they exist to prove. Verified against a compiler built under - # core=normal, so it is the runtime and not the build. + # core=auto leaves real mode on the interpreter, which is where the + # 16-bit compiler build spends its time. Nothing here is timing + # sensitive -- a compiler and a batch file -- so ask for the + # recompiler explicitly. The M3 bounds cases confirm abort() still + # reports its exit status under it. f"[log]\nlogfile={console}\n" - f"[dosbox]\nlog console=quiet\n", + f"[dosbox]\nlog console=quiet\n" + f"[cpu]\ncore=dynamic\ncycles=max\n", encoding="ascii", ) if cached.is_file(): From 3091df7a7819236faa6be07b2c8bc01d9abc5a19 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:45:04 +0900 Subject: [PATCH 103/184] refactor: unify the M1-M7 checker check_m7.c textually included check.c, renamed three entry points aside, and selected between two whole checkers by scanning each unit for `?`, `!`, `try` and friends. A unit that mentioned any of them was checked by a second implementation, so an M1-M6 rule fixed in check.c never reached it -- and the split hid real defects, since the M7 half was reached by no existing fixture until M7 cases were registered. The split was cheaper to undo than it looked: every M7 dispatcher already delegated to its M6 counterpart for nodes it did not handle. So the M7 entry points become the single check_expr/check_stmt/check_lvalue/check_call, and the former M6 bodies become check_expr_core/check_stmt_core/check_lvalue_core, reached as the fallback. Recursion runs through the unified entry, which is what makes an optional nested inside otherwise-M6 code get checked at all. m7_check_fn and m7_check_method were identical to the M6 versions apart from which check_stmt they called, so they are dropped. The feature scanner (m7_type_ast, m7_node_feature, m7_program_feature) is gone with the dispatch it fed, including the loop case that still consulted it. fe_check_program and fe_check_expr_type keep the M7 bodies, which are supersets. The build compiles check.c directly again. No behaviour intended to change: the unified checker applies the union of the rules to every unit, which for M1-M6 sources is what the M6 half already did. --- fec/Makefile | 2 +- fec/build-dos.bat | 2 +- fec/src/check.c | 1065 +++++++++++++++++++++++++++++++++++++--- fec/src/check_m7.c | 1154 -------------------------------------------- 4 files changed, 1009 insertions(+), 1214 deletions(-) delete mode 100644 fec/src/check_m7.c diff --git a/fec/Makefile b/fec/Makefile index cd71ce6..92e678b 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -std=c89 CPPFLAGS ?= -Isrc -SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check_m7.c src/lower.c src/emitcm7.c src/driver.c +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check.c src/lower.c src/emitcm7.c src/driver.c OBJ = $(SRC:.c=.o) .PHONY: all clean dos-build diff --git a/fec/build-dos.bat b/fec/build-dos.bat index 104d28f..627336d 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -27,7 +27,7 @@ wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=m7.obj src\m7.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=own.obj src\own.c if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check_m7.c +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lower.obj src\lower.c if errorlevel 1 goto build_fail diff --git a/fec/src/check.c b/fec/src/check.c index 415ffcb..7eaf64a 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -1,4 +1,5 @@ #include "check.h" +#include "m7.h" #include "own.h" #include #include @@ -298,6 +299,11 @@ static FeType *method_type(FeCheck *c, FeNode *node, FeType *owner) } static void check_match(FeCheckerState *s, FeNode *n); static void check_stmt(FeCheckerState *s, FeNode *n); +static FeType *check_expr_core(FeCheckerState *s, FeNode *n); +static void check_stmt_core(FeCheckerState *s, FeNode *n); +static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read); +static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read); +static FeType *check_call(FeCheckerState *s, FeNode *n); typedef struct FeFlowSlot { FeSym *sym; @@ -827,7 +833,7 @@ static FeType *check_identifier(FeCheckerState *s, FeNode *n) return sym->type; } -static FeType *check_expr(FeCheckerState *s, FeNode *n) +static FeType *check_expr_core(FeCheckerState *s, FeNode *n) { FeCheck *c = s->c; FeType *a; @@ -1154,7 +1160,7 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) return unknown(c); } -static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) +static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read) { FeSym *sym; FeType *base; @@ -1454,7 +1460,7 @@ static int own_return_from_allowed_root(FeCheckerState *s, FeNode *expr) return refs==1; } -static void check_stmt(FeCheckerState *s, FeNode *n) +static void check_stmt_core(FeCheckerState *s, FeNode *n) { FeCheck *c = s->c; FeScope *old; @@ -1752,91 +1758,1034 @@ static void check_method(FeCheck *c, FeNode *fn, FeScope *globals, if(fn->c) check_stmt(&s,fn->c); } +static int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) +{ + if (fe_type_equal(want,got)) return 1; + return compatible(want,got,value); +} + +static FeType *m7_check_expected(FeCheckerState *s, FeNode *value, + FeType *expected) +{ + FeType *actual; + FeM7ContextKind context; + if (!value) return unknown(s->c); + if (fe_m7_is_null(value)) { + if (!fe_m7_can_contextual_null(expected)) { + err(s->c,value->loc,"null requires a contextual optional type"); + value->sem_type=unknown(s->c); + return value->sem_type; + } + value->sem_type=expected; + value->sem_context=expected; + return expected; + } + actual=check_expr(s,value); + if (!expected) return actual; + if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && + m7_actual_compatible(expected->elem,actual,value)) { + value->sem_context=expected; + return expected; + } + if (expected->kind==FE_TYPE_ERROR_UNION) { + context=fe_m7_error_context(&s->c->types,expected,actual); + if (context!=FE_M7_CONTEXT_NONE) { + value->sem_context=expected; + return expected; + } + } + return actual; +} + +static FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) +{ + FeFieldType *field; + FeType *owner; + if (!base) return unknown(s->c); + if (n->text && strcmp(n->text,".?")==0) { + if (base->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional projection '.?' requires an optional value"); + return unknown(s->c); + } + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"unknown struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + if (base->kind==FE_TYPE_ENUM && n->b && n->b->text) { + if (!fe_type_variant(base,n->b->text)) + err(s->c,n->loc,"unknown enum variant"); + n->sem_type=base; + return base; + } + if ((base->kind==FE_TYPE_SLICE || base->kind==FE_TYPE_STR) && + n->b && n->b->text && strcmp(n->b->text,"n")==0) { + n->sem_type=fe_type_intern(&s->c->types,"usize"); + return n->sem_type; + } + n->sem_type=unknown(s->c); + return n->sem_type; +} + +static int m7_place_is_projection(FeNode *n) +{ + return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); +} + +static FeType *check_call(FeCheckerState *s, FeNode *n) +{ + FeCheck *c; + FeNode *arg; + FeNode *value; + FeNode *param; + FeSym *sym; + FeType *a; + FeType *b; + FeType *expected; + c=s->c; + if (n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"Some")==0) { + err(c,n->loc,"Some is only valid as an optional pattern"); + n->sem_type=unknown(c); + return n->sem_type; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { + arg=n->children; + if (strcmp(n->a->b->text,"replace")==0) { + value=arg ? arg->next : 0; + if (!arg || !value || value->next) { + err(c,n->loc,"mem.replace requires destination and value"); + n->sem_type=unknown(c); + return n->sem_type; + } + a=check_expr(s,arg); + if (!a || a->kind!=FE_TYPE_REF || !a->ref_mut || + !arg->a || !lvalue_writable(s,arg->a)) + err(c,n->loc,"mem.replace destination must be a mutable place"); + expected=a && a->kind==FE_TYPE_REF ? a->elem : 0; + b=m7_check_expected(s,value,expected); + if (expected && !fe_type_equal(expected,b) && + !m7_actual_compatible(expected,b,value)) + err(c,value->loc,"mem.replace value type mismatch"); + mark_moved(s,value,value->sem_type ? value->sem_type : b); + n->sem_type=expected ? expected : unknown(c); + fe_type_require_replace(&c->types,n->sem_type); + return n->sem_type; + } + if (strcmp(n->a->b->text,"destroy")==0) { + a=arg ? check_expr(s,arg) : unknown(c); + if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) + err(c,n->loc,"mem.destroy requires exactly one owned pointer"); + else mark_moved(s,arg,a); + n->sem_type=fe_type_intern(&c->types,"void"); + return n->sem_type; + } + if (strcmp(n->a->b->text,"create")==0 || + strcmp(n->a->b->text,"alloc_slice")==0) + return check_expr_core(s,n); + } + if (n->a && n->a->kind==FE_N_IDENT) { + sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); + if (!sym || !sym->fn) { + err(c,n->loc,"unknown function"); + n->sem_type=unknown(c); + return n->sem_type; + } + n->a->cname=sym->cname; + n->sem_decl=sym->fn; + param=sym->fn->a ? sym->fn->a->children : 0; + arg=n->children; + while (param && arg) { + b=node_type(c,param->a); + a=m7_check_expected(s,arg,b); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root; + root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (!(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut)) + mark_moved(s,arg,arg->sem_type ? arg->sem_type : a); + if (!fe_type_equal(b,a) && !m7_actual_compatible(b,a,arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind!=FE_TYPE_UNKNOWN) + err(c,arg->loc,"argument type mismatch"); + own_release_temporary_borrow(s,arg); + param=param->next; + arg=arg->next; + } + if (param || arg) err(c,n->loc,"wrong number of arguments"); + n->sem_type=sym->fn->b ? node_type(c,sym->fn->b) : + fe_type_intern(&c->types,"void"); + return n->sem_type; + } + return check_expr_core(s,n); +} + +static void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, + FeOwnState **own, FeFlowBorrow **borrow, + unsigned *count) +{ + *count=flow_capture(s->scope,slots,FE_M7_FLOW_CAP); + *own=flow_own_new(s,*count); + *borrow=flow_borrow_new(s,*count); + flow_own_capture(slots,*own,*count); + flow_borrow_capture(slots,*borrow,*count); +} + +static void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, + FeFlowBorrow *borrow, unsigned count) +{ + flow_restore(slots,count); + flow_own_restore(slots,own,count); + flow_borrow_restore(slots,borrow,count); +} + +static void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, + FeOwnState *own_base, + FeFlowBorrow *borrow_base, + unsigned count, FeFlowSlot *rhs, + FeOwnState *own_rhs, + FeFlowBorrow *borrow_rhs) +{ + (void)s; + flow_merge(base,base,rhs,count); + flow_own_merge(base,own_base,own_rhs,count); + flow_borrow_merge(base,borrow_base,borrow_rhs,count); +} + +static int m7_stmt_definitely_exits(FeNode *n) +{ + FeNode *last; + if (!n) return 0; + if (n->kind==FE_N_RETURN || n->kind==FE_N_BREAK || + n->kind==FE_N_CONTINUE) return 1; + if (n->kind==FE_N_BLOCK) { + last=n->children; + if (!last) return 0; + while (last->next) last=last->next; + return m7_stmt_definitely_exits(last); + } + if (n->kind==FE_N_IF && n->b && n->c) + return m7_stmt_definitely_exits(n->b) && + m7_stmt_definitely_exits(n->c); + return 0; +} + +static FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, + FeM7LazyKind kind) +{ + FeType *left_type; + FeType *payload; + FeType *right_type; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot rhs[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_rhs; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_rhs; + unsigned count; + unsigned rhs_count; + FeScope *old; + FeType *error_type; + left_type=check_expr(s,n->a); + if (kind==FE_M7_LAZY_ORELSE) { + if (!left_type || left_type->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"orelse requires an optional left operand"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->elem; + if (!fe_own_is_copy_type(payload)) { + if (m7_place_is_projection(n->a)) + err(s->c,n->loc, + "non-Copy optional projection requires mem.replace before orelse"); + else + mark_moved(s,n->a,left_type); + } + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"orelse fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; + } + if (!left_type || left_type->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"catch requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->error_value; + mark_moved(s,n->a,left_type); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + if (n->c) { + old=s->scope; + s->scope=scope_new(s,old); + error_type=fe_m7_error_type(&s->c->types,left_type); + if (n->b && n->b->text) + add_symbol(s,s->scope,n->b->text,error_type,0,0,1, + local_cname(s->c,n->b->text),n->b); + check_stmt(s,n->c); + s->scope=old; + if (payload && payload->kind!=FE_TYPE_VOID && + !m7_stmt_definitely_exits(n->c)) + err(s->c,n->loc, + "catch block for a value result must exit instead of falling through"); + if (payload && payload->kind==FE_TYPE_VOID) { + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + } else { + m7_restore_flow(base,own_base,borrow_base,count); + } + n->sem_type=payload; + return payload; + } + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"catch fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; +} + +static FeType *check_expr(FeCheckerState *s, FeNode *n) +{ + FeType *a; + FeType *b; + FeType *ret_error; + FeType *got_error; + FeM7LazyKind lazy; + const char *op; + if (!n) return unknown(s->c); + if (fe_m7_is_null(n)) { + err(s->c,n->loc,"null requires a contextual optional type"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (n->kind==FE_N_IDENT) + return check_identifier(s,n); + if (n->kind==FE_N_LITERAL) + return check_expr_core(s,n); + if (n->kind==FE_N_CALL) + return check_call(s,n); + if (n->kind==FE_N_MEMBER) { + a=check_expr(s,n->a); + return m7_member_field(s,n,a); + } + if (n->kind==FE_N_INDEX) + return check_index(s,n); + if (n->kind==FE_N_UNARY) { + op=n->text ? n->text : ""; + if (strcmp(op,"try")==0) { + a=check_expr(s,n->a); + if (!a || a->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an enclosing error result"); + } else { + ret_error=fe_m7_error_type(&s->c->types,s->ret); + got_error=fe_m7_error_type(&s->c->types,a); + if (!ret_error || !got_error || !fe_type_equal(ret_error,got_error)) + err(s->c,n->loc, + "try error type must exactly match the enclosing error result"); + } + mark_moved(s,n->a,a); + n->sem_type=a->error_value; + return n->sem_type; + } + if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + a=check_expr(s,n->a); + own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); + n->sem_type=fe_type_ref(&s->c->types,a,strcmp(op,"&mut")==0); + return n->sem_type; + } + return check_expr_core(s,n); + } + if (n->kind==FE_N_BINARY) { + lazy=fe_m7_lazy_kind(n); + if (lazy!=FE_M7_LAZY_NONE) + return m7_check_lazy(s,n,lazy); + op=n->text ? n->text : ""; + if ((strcmp(op,"==")==0 || strcmp(op,"!=")==0) && + (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { + FeNode *nonnull; + FeNode *nullnode; + nonnull=fe_m7_is_null(n->a) ? n->b : n->a; + nullnode=fe_m7_is_null(n->a) ? n->a : n->b; + a=check_expr(s,nonnull); + if (!a || a->kind!=FE_TYPE_OPTIONAL) + err(s->c,n->loc,"null comparison requires an optional value"); + else { + nullnode->sem_type=a; + nullnode->sem_context=a; + } + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + a=check_expr(s,n->a); + b=check_expr(s,n->b); + if (strcmp(op,"and")==0 || strcmp(op,"or")==0) { + if ((known(a) && a->kind!=FE_TYPE_BOOL) || + (known(b) && b->kind!=FE_TYPE_BOOL)) + err(s->c,n->loc,"logical operator requires bool operands"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + if (strcmp(op,"==")==0 || strcmp(op,"!=")==0 || + strcmp(op,"<")==0 || strcmp(op,"<=")==0 || + strcmp(op,">")==0 || strcmp(op,">=")==0) { + if (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a)) + err(s->c,n->loc,"comparison operands have different types"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b)) || + (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a))) + err(s->c,n->loc,"arithmetic operands must have the same integer type"); + n->sem_type=a; + return a; + } + if (n->kind==FE_N_TYPE && n->text && strcmp(n->text,"as")==0) + return check_expr_core(s,n); + if (n->kind==FE_N_STRUCT_INIT) + return check_struct_init(s,n); + if (n->kind==FE_N_ARRAY_INIT) + return check_array_init(s,n); + return check_expr_core(s,n); +} + +static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) +{ + FeType *base; + FeFieldType *field; + FeType *owner; + if (!n) return unknown(s->c); + if (n->kind==FE_N_MEMBER) { + base=check_expr(s,n->a); + if (base && base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base && base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + if (!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + n->sem_type=base->elem; + return base->elem; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + if (base->kind==FE_TYPE_REF && !base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"assignment requires a valid struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + } + return check_lvalue_core(s,n,read); +} + +static FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, + FeNode *source, int *borrow_mut) +{ + FeSym *root; + int mutable; + *borrow_mut=0; + if (fe_own_is_copy_type(payload)) return payload; + root=own_root_symbol(s,source); + mutable=root && root->mutable; + *borrow_mut=mutable; + if (payload->kind==FE_TYPE_OWNED && payload->elem) + return fe_type_ref(&s->c->types,payload->elem,mutable); + return fe_type_ref(&s->c->types,payload,mutable); +} + +static void m7_check_if_let(FeCheckerState *s, FeNode *n) +{ + FeType *opt; + FeType *binding_type; + FeNode *binding; + FeSym *root; + FeScope *old; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + int borrow_mut; + int is_some; + opt=check_expr(s,n->a); + if (!opt || opt->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"if let Some/None requires an optional value"); + return; + } + is_some=n->aux_text && strcmp(n->aux_text,"Some")==0; + if (!is_some && (!n->aux_text || strcmp(n->aux_text,"None")!=0)) + err(s->c,n->loc,"if let optional pattern must be Some or None"); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + binding=n->children; + if (is_some && binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + n->loc); + } + } + check_stmt(s,n->b); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + m7_restore_flow(base,own_base,borrow_base,count); + if (n->c) check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + } else { + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + for (i=0;ichildren;arm;arm=arm->next) { + m7_restore_flow(base,own_base,borrow_base,count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + if (arm->text && strcmp(arm->text,"Some")==0) { + if (seen_some) err(s->c,arm->loc,"duplicate Some match arm"); + seen_some=1; + binding=arm->children; + if (binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + arm->loc); + } + } + } else if (arm->text && strcmp(arm->text,"None")==0) { + if (seen_none) err(s->c,arm->loc,"duplicate None match arm"); + seen_none=1; + } else if (arm->text && strcmp(arm->text,"_")==0) { + wildcard=1; + } else { + err(s->c,arm->loc,"optional match arm must be Some, None, or _"); + } + if (arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); + else if (arm->a) check_expr(s,arm->a); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,current,count); + own_current=flow_own_new(s,count); + borrow_current=flow_borrow_new(s,count); + flow_own_capture(current,own_current,count); + flow_borrow_capture(current,borrow_current,count); + if (!have) { + for (i=0;ic,n->loc,"non-exhaustive optional match"); + if (have) m7_restore_flow(merged,own_merged,borrow_merged,count); +} + +static void m7_check_match_stmt(FeCheckerState *s, FeNode *n) +{ + FeType *value; + value=check_expr(s,n->a); + if (value && value->kind==FE_TYPE_OPTIONAL) { + m7_check_optional_match(s,n,value); + n->sem_type=unknown(s->c); + return; + } + check_match(s,n); +} + +static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) +{ + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + int initialized; + expected=n->a ? node_type(s->c,n->a) : 0; + if (n->b) + stored=m7_check_expected(s,n->b,expected); + else + stored=expected ? expected : unknown(s->c); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!expected) expected=stored; + if (!n->a && n->b && fe_m7_is_null(n->b)) + err(s->c,n->loc,"null initializer requires an explicit optional type"); + if (expected && expected->kind==FE_TYPE_VOID) + err(s->c,n->loc,"variable cannot have void type"); + if (n->b && !fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) + err(s->c,n->loc,"initializer type mismatch"); + if (n->b) mark_moved(s,n->b,actual); + initialized=n->b!=0; + sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, + local_cname(s->c,n->text ? n->text : "local"),n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth!=0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); +} + +static void check_stmt(FeCheckerState *s, FeNode *n) +{ + FeScope *old; + FeNode *x; + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + old=s->scope; + s->scope=scope_new(s,old); + for (x=n->children;x;x=x->next) { + check_stmt(s,x); + own_release_after_stmt(s,s->scope,x,0); + } + own_release_after_stmt(s,s->scope,n,1); + s->scope=old; + break; + case FE_N_LET: + case FE_N_CONST: + m7_check_decl_stmt(s,n,0); + break; + case FE_N_VAR: + if (!n->a && !n->b) + err(s->c,n->loc,"uninitialized var requires an explicit type"); + m7_check_decl_stmt(s,n,1); + break; + case FE_N_ASSIGN: + expected=check_lvalue(s,n->a,compound_operator(n->text)); + stored=m7_check_expected(s,n->b,expected); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) + err(s->c,n->loc,"assignment type mismatch"); + mark_moved(s,n->b,actual); + sym=n->a && n->a->kind==FE_N_IDENT ? + find_symbol(s->scope,n->a->text) : 0; + if (sym && sym->mutable) { + sym->initialized=1; + fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); + sym->moved=sym->own.move; + } + break; + case FE_N_EXPR_STMT: + check_expr(s,n->a); + break; + case FE_N_DEFER: + ++s->defer_depth; + check_stmt(s,n->a); + --s->defer_depth; + break; + case FE_N_IF: + if (n->text && strcmp(n->text,"if let")==0) + m7_check_if_let(s,n); + else { + FeType *cond; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + cond=check_expr(s,n->a); + if (known(cond) && cond->kind!=FE_TYPE_BOOL) + err(s->c,n->loc,"if condition must be bool"); + /* Once a function is on the M7 checker path, branch bodies must + remain on that path as well. In particular, return T inside + E!T relies on contextual success construction even when the + branch itself contains no surface M7 syntax. */ + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + check_stmt(s,n->b); + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + m7_restore_flow(base,own_base,borrow_base,count); + if (n->c) check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + } else { + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + for (i=0;iret; + if (n->a) + stored=m7_check_expected(s,n->a,expected); + else + stored=fe_type_intern(&s->c->types,"void"); + actual=n->a && n->a->sem_type ? n->a->sem_type : stored; + if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a && + actual && actual->kind==FE_TYPE_ERROR_UNION && + !fe_type_equal(expected,actual)) + err(s->c,n->loc,"error result type mismatch"); + else if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->a)) + err(s->c,n->loc,"return type mismatch"); + if (n->a) mark_moved(s,n->a,actual); + break; + case FE_N_WHILE: + case FE_N_FOR: + /* One loop rule now that there is one checker: the body recurses + through this function, so any expression in it is checked the same + way whether or not the unit mentions optionals or error unions. */ + if (n->kind==FE_N_WHILE) { + actual=check_expr(s,n->a); + if (known(actual) && actual->kind!=FE_TYPE_BOOL) + err(s->c,n->loc,"while condition must be bool"); + ++s->loop_depth; + check_stmt(s,n->b); + --s->loop_depth; + } else check_for(s,n); + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (!s->loop_depth) + err(s->c,n->loc,"break or continue outside loop"); + break; + case FE_N_UNSAFE: + check_stmt(s,n->a); + break; + default: + check_stmt_core(s,n); + break; + } +} + +static int m7_ast_reference_storage(FeNode *type) +{ + if (!type || !type->text) return 0; + if (strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || + (strcmp(type->text,"[")==0 && !type->a) || + strcmp(type->text,"str")==0) + return 1; + if (strcmp(type->text,"?")==0) + return m7_ast_reference_storage(type->a); + if (strcmp(type->text,"^")==0) return 0; + return 0; +} + +static void m7_check_storage(FeCheck *c, FeNode *decl) +{ + FeNode *m; + if (!decl) return; + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + for (m=decl->children;m;m=m->next) + if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a)) + err(c,m->loc,"reference type is not allowed in aggregate storage"); + } + check_reference_storage(c,decl); +} + +static void m7_validate_error_decl(FeCheck *c, FeNode *decl) +{ + FeNode *a; + FeNode *b; + unsigned long code; + unsigned long other; + if (!decl || decl->kind!=FE_N_ERROR_DECL) return; + for (a=decl->children;a;a=a->next) { + if (!a->a || a->a->kind!=FE_N_LITERAL || !a->a->text) continue; + code=strtoul(a->a->text,0,0); + if (code==0UL) + err(c,a->loc,"error code 0 is reserved for success"); + for (b=decl->children;b && b!=a;b=b->next) { + if (a->text && b->text && strcmp(a->text,b->text)==0) { + err(c,a->loc,"duplicate error member name"); + break; + } + if (b->a && b->a->kind==FE_N_LITERAL && b->a->text) { + other=strtoul(b->a->text,0,0); + if (other==code) { + err(c,a->loc,"duplicate error numeric code"); + break; + } + } + } + } +} + int fe_check_program(FeCheck *c) { FeCheckerState s; FeNode *n; + FeNode *m; FeSym *sym; FeType *t; FeType *iv; - s.c = c; - s.scope = scope_new(&s, 0); - s.globals = s.scope; - s.ret = fe_type_intern(&c->types, "void"); - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) - if (n->kind == FE_N_STRUCT) - fe_type_declare_struct(&c->types, n, (n->flags & 1U) != 0); - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) - check_reference_storage(c,n); - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) - if (n->kind == FE_N_ENUM) fe_type_declare_enum(&c->types, n); - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) - if (n->kind == FE_N_ERROR_DECL) fe_type_declare_error(&c->types, n); + char method_name[128]; + s.c=c; + s.scope=scope_new(&s,0); + s.globals=s.scope; + s.ret=fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=0; + fe_own_liveness_init(&s.liveness,&c->ast->arena); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) + fe_type_declare_struct(&c->types,n,(n->flags & 1U)!=0); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + m7_check_storage(c,n); + if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ENUM) fe_type_declare_enum(&c->types,n); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); check_type_cycles(c); fe_type_layout_all(&c->types); - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { - if(n->kind==FE_N_STRUCT) { - FeNode *m; - char method_name[128]; - for(m=n->children; m; m=m->next) if(m->kind==FE_N_FN) { + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + if (n->kind==FE_N_STRUCT) { + for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) { sprintf(method_name,"%s_%s",n->text ? n->text : "Type", m->text ? m->text : "method"); m->cname=unit_cname(c,method_name); } } - if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { - t = n->a ? node_type(c, n->a) : unknown(c); - add_symbol(&s, s.globals, n->text, t, 0, - n->kind == FE_N_GLOBAL, n->b != 0, - unit_cname(c, n->text ? n->text : "global"), n); + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + t=n->a ? node_type(c,n->a) : unknown(c); + add_symbol(&s,s.globals,n->text,t,0,n->kind==FE_N_GLOBAL, + n->b!=0,unit_cname(c,n->text ? n->text : "global"),n); } } - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { - if (n->kind == FE_N_FN) { - t = fe_type_intern(&c->types, ""); - add_symbol(&s, s.globals, n->text, t, n, 0, 1, - unit_cname(c, n->text ? n->text : "fn"), n); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) { + t=fe_type_intern(&c->types,""); + add_symbol(&s,s.globals,n->text,t,n,0,1, + unit_cname(c,n->text ? n->text : "fn"),n); } - } - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) { - if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { - sym = find_current(s.globals, n->text ? n->text : ""); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + sym=find_current(s.globals,n->text ? n->text : ""); if (n->b) { - iv = check_expr(&s, n->b); - if (sym && sym->type->kind == FE_TYPE_UNKNOWN) { - sym->type = iv; - n->sem_type = iv; - } else if (sym && !compatible(sym->type, iv, n->b) && - iv->kind != FE_TYPE_UNKNOWN) - err(c, n->loc, "global initializer type mismatch"); - if (iv->kind == FE_TYPE_VOID) - err(c, n->loc, "void expression cannot initialize a global"); + iv=m7_check_expected(&s,n->b,sym ? sym->type : 0); + if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { + sym->type=iv; + n->sem_type=iv; + } else if (sym && !fe_type_equal(sym->type,iv) && + !m7_actual_compatible(sym->type,iv,n->b)) + err(c,n->loc,"global initializer type mismatch"); } } - } - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) - if (n->kind == FE_N_FN) check_fn(c, n, s.globals); - for (n = c->ast->root ? c->ast->root->children : 0; n; n = n->next) - if(n->kind==FE_N_STRUCT) { - FeNode *m; + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) check_fn(c,n,s.globals); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) { t=fe_type_intern(&c->types,n->text); - for(m=n->children; m; m=m->next) - if(m->kind==FE_N_FN) check_method(c,m,s.globals,t); + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) check_method(c,m,s.globals,t); } fe_type_layout_all(&c->types); - return c->diags->errors == 0; + return c->diags->errors==0; } FeType *fe_check_expr_type(FeCheck *c, FeNode *n) { FeCheckerState s; - s.c = c; - s.scope = scope_new(&s, 0); - s.globals = s.scope; - s.ret = fe_type_intern(&c->types, "void"); + s.c=c; + s.scope=scope_new(&s,0); + s.globals=s.scope; + s.ret=fe_type_intern(&c->types,"void"); s.loop_depth=0; s.defer_depth=0; s.fn_node=0; fe_own_liveness_init(&s.liveness,&c->ast->arena); - return check_expr(&s, n); + return check_expr(&s,n); } diff --git a/fec/src/check_m7.c b/fec/src/check_m7.c deleted file mode 100644 index 34b0388..0000000 --- a/fec/src/check_m7.c +++ /dev/null @@ -1,1154 +0,0 @@ -/* Compiler-A M7 integration. The verified M1-M6 checker remains the exact - fast path for sources that do not use M7 syntax or types. M7 sources reuse - its symbol, ownership and flow helpers from this translation unit. */ -#define fe_check_init fe_check_init_m6 -#define fe_check_program fe_check_program_m6 -#define fe_check_expr_type fe_check_expr_type_m6 -#include "check.c" -#undef fe_check_init -#undef fe_check_program -#undef fe_check_expr_type - -#include "m7.h" -#include - -#define FE_M7_FLOW_CAP 64U - -static FeType *m7_check_expr(FeCheckerState *s, FeNode *n); -static void m7_check_stmt(FeCheckerState *s, FeNode *n); - -static int m7_type_ast(const FeNode *n) -{ - const FeNode *x; - if (!n) return 0; - if (n->kind==FE_N_TYPE && n->text && - (strcmp(n->text,"?")==0 || strcmp(n->text,"!")==0)) - return 1; - if (m7_type_ast(n->a) || m7_type_ast(n->b) || m7_type_ast(n->c)) - return 1; - for (x=n->children;x;x=x->next) - if (m7_type_ast(x)) return 1; - return 0; -} - -static int m7_node_feature(const FeNode *n) -{ - const FeNode *x; - if (!n) return 0; - if (n->kind==FE_N_ERROR_DECL) return 1; - if (fe_m7_is_null(n) || fe_m7_is_try(n)) return 1; - if (n->kind==FE_N_MEMBER && n->text && strcmp(n->text,".?")==0) - return 1; - if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) - return 1; - if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) - return 1; - if (n->kind==FE_N_ARM && n->text && - (strcmp(n->text,"Some")==0 || strcmp(n->text,"None")==0)) - return 1; - if (m7_type_ast(n)) return 1; - if (m7_node_feature(n->a) || m7_node_feature(n->b) || - m7_node_feature(n->c)) return 1; - for (x=n->children;x;x=x->next) - if (m7_node_feature(x)) return 1; - return 0; -} - -static int m7_program_feature(FeCheck *c) -{ - return c && c->ast && m7_node_feature(c->ast->root); -} - -static int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) -{ - if (fe_type_equal(want,got)) return 1; - return compatible(want,got,value); -} - -static FeType *m7_check_expected(FeCheckerState *s, FeNode *value, - FeType *expected) -{ - FeType *actual; - FeM7ContextKind context; - if (!value) return unknown(s->c); - if (fe_m7_is_null(value)) { - if (!fe_m7_can_contextual_null(expected)) { - err(s->c,value->loc,"null requires a contextual optional type"); - value->sem_type=unknown(s->c); - return value->sem_type; - } - value->sem_type=expected; - value->sem_context=expected; - return expected; - } - actual=m7_check_expr(s,value); - if (!expected) return actual; - if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && - m7_actual_compatible(expected->elem,actual,value)) { - value->sem_context=expected; - return expected; - } - if (expected->kind==FE_TYPE_ERROR_UNION) { - context=fe_m7_error_context(&s->c->types,expected,actual); - if (context!=FE_M7_CONTEXT_NONE) { - value->sem_context=expected; - return expected; - } - } - return actual; -} - -static FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) -{ - FeFieldType *field; - FeType *owner; - if (!base) return unknown(s->c); - if (n->text && strcmp(n->text,".?")==0) { - if (base->kind!=FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"optional projection '.?' requires an optional value"); - return unknown(s->c); - } - n->sem_type=base->elem; - return n->sem_type; - } - if (base->kind==FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"optional value must be projected with '.?' first"); - return unknown(s->c); - } - if (base->kind==FE_TYPE_REF && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - n->sem_type=base->elem; - return n->sem_type; - } - if (base->kind==FE_TYPE_OWNED && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - n->sem_type=base->elem; - return n->sem_type; - } - owner=base; - if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && - base->elem && base->elem->kind==FE_TYPE_STRUCT) - owner=base->elem; - if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { - field=fe_type_field(owner,n->b->text); - if (!field) { - err(s->c,n->loc,"unknown struct field"); - return unknown(s->c); - } - n->sem_type=field->type; - return field->type; - } - if (base->kind==FE_TYPE_ENUM && n->b && n->b->text) { - if (!fe_type_variant(base,n->b->text)) - err(s->c,n->loc,"unknown enum variant"); - n->sem_type=base; - return base; - } - if ((base->kind==FE_TYPE_SLICE || base->kind==FE_TYPE_STR) && - n->b && n->b->text && strcmp(n->b->text,"n")==0) { - n->sem_type=fe_type_intern(&s->c->types,"usize"); - return n->sem_type; - } - n->sem_type=unknown(s->c); - return n->sem_type; -} - -static int m7_place_is_projection(FeNode *n) -{ - return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); -} - -static FeType *m7_check_call(FeCheckerState *s, FeNode *n) -{ - FeCheck *c; - FeNode *arg; - FeNode *value; - FeNode *param; - FeSym *sym; - FeType *a; - FeType *b; - FeType *expected; - c=s->c; - if (n->a && n->a->kind==FE_N_IDENT && n->a->text && - strcmp(n->a->text,"Some")==0) { - err(c,n->loc,"Some is only valid as an optional pattern"); - n->sem_type=unknown(c); - return n->sem_type; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { - arg=n->children; - if (strcmp(n->a->b->text,"replace")==0) { - value=arg ? arg->next : 0; - if (!arg || !value || value->next) { - err(c,n->loc,"mem.replace requires destination and value"); - n->sem_type=unknown(c); - return n->sem_type; - } - a=m7_check_expr(s,arg); - if (!a || a->kind!=FE_TYPE_REF || !a->ref_mut || - !arg->a || !lvalue_writable(s,arg->a)) - err(c,n->loc,"mem.replace destination must be a mutable place"); - expected=a && a->kind==FE_TYPE_REF ? a->elem : 0; - b=m7_check_expected(s,value,expected); - if (expected && !fe_type_equal(expected,b) && - !m7_actual_compatible(expected,b,value)) - err(c,value->loc,"mem.replace value type mismatch"); - mark_moved(s,value,value->sem_type ? value->sem_type : b); - n->sem_type=expected ? expected : unknown(c); - fe_type_require_replace(&c->types,n->sem_type); - return n->sem_type; - } - if (strcmp(n->a->b->text,"destroy")==0) { - a=arg ? m7_check_expr(s,arg) : unknown(c); - if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) - err(c,n->loc,"mem.destroy requires exactly one owned pointer"); - else mark_moved(s,arg,a); - n->sem_type=fe_type_intern(&c->types,"void"); - return n->sem_type; - } - if (strcmp(n->a->b->text,"create")==0 || - strcmp(n->a->b->text,"alloc_slice")==0) - return check_expr(s,n); - } - if (n->a && n->a->kind==FE_N_IDENT) { - sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); - if (!sym || !sym->fn) { - err(c,n->loc,"unknown function"); - n->sem_type=unknown(c); - return n->sem_type; - } - n->a->cname=sym->cname; - n->sem_decl=sym->fn; - param=sym->fn->a ? sym->fn->a->children : 0; - arg=n->children; - while (param && arg) { - b=node_type(c,param->a); - a=m7_check_expected(s,arg,b); - if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && - a->kind==FE_TYPE_REF && a->ref_mut) { - FeSym *root; - root=own_root_symbol(s,arg); - if (root && root->borrow_root) root=root->borrow_root; - if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); - } else if (!(b && a && b->kind==FE_TYPE_SLICE && - a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut)) - mark_moved(s,arg,arg->sem_type ? arg->sem_type : a); - if (!fe_type_equal(b,a) && !m7_actual_compatible(b,a,arg) && - !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - a->kind!=FE_TYPE_UNKNOWN) - err(c,arg->loc,"argument type mismatch"); - own_release_temporary_borrow(s,arg); - param=param->next; - arg=arg->next; - } - if (param || arg) err(c,n->loc,"wrong number of arguments"); - n->sem_type=sym->fn->b ? node_type(c,sym->fn->b) : - fe_type_intern(&c->types,"void"); - return n->sem_type; - } - return check_expr(s,n); -} - -static void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, - FeOwnState **own, FeFlowBorrow **borrow, - unsigned *count) -{ - *count=flow_capture(s->scope,slots,FE_M7_FLOW_CAP); - *own=flow_own_new(s,*count); - *borrow=flow_borrow_new(s,*count); - flow_own_capture(slots,*own,*count); - flow_borrow_capture(slots,*borrow,*count); -} - -static void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, - FeFlowBorrow *borrow, unsigned count) -{ - flow_restore(slots,count); - flow_own_restore(slots,own,count); - flow_borrow_restore(slots,borrow,count); -} - -static void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, - FeOwnState *own_base, - FeFlowBorrow *borrow_base, - unsigned count, FeFlowSlot *rhs, - FeOwnState *own_rhs, - FeFlowBorrow *borrow_rhs) -{ - (void)s; - flow_merge(base,base,rhs,count); - flow_own_merge(base,own_base,own_rhs,count); - flow_borrow_merge(base,borrow_base,borrow_rhs,count); -} - -static int m7_stmt_definitely_exits(FeNode *n) -{ - FeNode *last; - if (!n) return 0; - if (n->kind==FE_N_RETURN || n->kind==FE_N_BREAK || - n->kind==FE_N_CONTINUE) return 1; - if (n->kind==FE_N_BLOCK) { - last=n->children; - if (!last) return 0; - while (last->next) last=last->next; - return m7_stmt_definitely_exits(last); - } - if (n->kind==FE_N_IF && n->b && n->c) - return m7_stmt_definitely_exits(n->b) && - m7_stmt_definitely_exits(n->c); - return 0; -} - -static FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, - FeM7LazyKind kind) -{ - FeType *left_type; - FeType *payload; - FeType *right_type; - FeFlowSlot base[FE_M7_FLOW_CAP]; - FeFlowSlot rhs[FE_M7_FLOW_CAP]; - FeOwnState *own_base; - FeOwnState *own_rhs; - FeFlowBorrow *borrow_base; - FeFlowBorrow *borrow_rhs; - unsigned count; - unsigned rhs_count; - FeScope *old; - FeType *error_type; - left_type=m7_check_expr(s,n->a); - if (kind==FE_M7_LAZY_ORELSE) { - if (!left_type || left_type->kind!=FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"orelse requires an optional left operand"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - payload=left_type->elem; - if (!fe_own_is_copy_type(payload)) { - if (m7_place_is_projection(n->a)) - err(s->c,n->loc, - "non-Copy optional projection requires mem.replace before orelse"); - else - mark_moved(s,n->a,left_type); - } - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - right_type=m7_check_expected(s,n->b,payload); - if (!fe_type_equal(payload,right_type) && - !m7_actual_compatible(payload,right_type,n->b)) - err(s->c,n->b ? n->b->loc : n->loc,"orelse fallback type mismatch"); - mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); - rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); - own_rhs=flow_own_new(s,rhs_count); - borrow_rhs=flow_borrow_new(s,rhs_count); - flow_own_capture(rhs,own_rhs,rhs_count); - flow_borrow_capture(rhs,borrow_rhs,rhs_count); - if (rhs_count==count) - m7_merge_rhs_flow(s,base,own_base,borrow_base,count, - rhs,own_rhs,borrow_rhs); - n->sem_type=payload; - return payload; - } - if (!left_type || left_type->kind!=FE_TYPE_ERROR_UNION) { - err(s->c,n->loc,"catch requires an error result"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - payload=left_type->error_value; - mark_moved(s,n->a,left_type); - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - if (n->c) { - old=s->scope; - s->scope=scope_new(s,old); - error_type=fe_m7_error_type(&s->c->types,left_type); - if (n->b && n->b->text) - add_symbol(s,s->scope,n->b->text,error_type,0,0,1, - local_cname(s->c,n->b->text),n->b); - m7_check_stmt(s,n->c); - s->scope=old; - if (payload && payload->kind!=FE_TYPE_VOID && - !m7_stmt_definitely_exits(n->c)) - err(s->c,n->loc, - "catch block for a value result must exit instead of falling through"); - if (payload && payload->kind==FE_TYPE_VOID) { - rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); - own_rhs=flow_own_new(s,rhs_count); - borrow_rhs=flow_borrow_new(s,rhs_count); - flow_own_capture(rhs,own_rhs,rhs_count); - flow_borrow_capture(rhs,borrow_rhs,rhs_count); - if (rhs_count==count) - m7_merge_rhs_flow(s,base,own_base,borrow_base,count, - rhs,own_rhs,borrow_rhs); - } else { - m7_restore_flow(base,own_base,borrow_base,count); - } - n->sem_type=payload; - return payload; - } - right_type=m7_check_expected(s,n->b,payload); - if (!fe_type_equal(payload,right_type) && - !m7_actual_compatible(payload,right_type,n->b)) - err(s->c,n->b ? n->b->loc : n->loc,"catch fallback type mismatch"); - mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); - rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); - own_rhs=flow_own_new(s,rhs_count); - borrow_rhs=flow_borrow_new(s,rhs_count); - flow_own_capture(rhs,own_rhs,rhs_count); - flow_borrow_capture(rhs,borrow_rhs,rhs_count); - if (rhs_count==count) - m7_merge_rhs_flow(s,base,own_base,borrow_base,count, - rhs,own_rhs,borrow_rhs); - n->sem_type=payload; - return payload; -} - -static FeType *m7_check_expr(FeCheckerState *s, FeNode *n) -{ - FeType *a; - FeType *b; - FeType *ret_error; - FeType *got_error; - FeM7LazyKind lazy; - const char *op; - if (!n) return unknown(s->c); - if (fe_m7_is_null(n)) { - err(s->c,n->loc,"null requires a contextual optional type"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - if (n->kind==FE_N_IDENT) - return check_identifier(s,n); - if (n->kind==FE_N_LITERAL) - return check_expr(s,n); - if (n->kind==FE_N_CALL) - return m7_check_call(s,n); - if (n->kind==FE_N_MEMBER) { - a=m7_check_expr(s,n->a); - return m7_member_field(s,n,a); - } - if (n->kind==FE_N_INDEX) - return check_index(s,n); - if (n->kind==FE_N_UNARY) { - op=n->text ? n->text : ""; - if (strcmp(op,"try")==0) { - a=m7_check_expr(s,n->a); - if (!a || a->kind!=FE_TYPE_ERROR_UNION) { - err(s->c,n->loc,"try requires an error result"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - if (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION) { - err(s->c,n->loc,"try requires an enclosing error result"); - } else { - ret_error=fe_m7_error_type(&s->c->types,s->ret); - got_error=fe_m7_error_type(&s->c->types,a); - if (!ret_error || !got_error || !fe_type_equal(ret_error,got_error)) - err(s->c,n->loc, - "try error type must exactly match the enclosing error result"); - } - mark_moved(s,n->a,a); - n->sem_type=a->error_value; - return n->sem_type; - } - if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { - a=m7_check_expr(s,n->a); - own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); - n->sem_type=fe_type_ref(&s->c->types,a,strcmp(op,"&mut")==0); - return n->sem_type; - } - return check_expr(s,n); - } - if (n->kind==FE_N_BINARY) { - lazy=fe_m7_lazy_kind(n); - if (lazy!=FE_M7_LAZY_NONE) - return m7_check_lazy(s,n,lazy); - op=n->text ? n->text : ""; - if ((strcmp(op,"==")==0 || strcmp(op,"!=")==0) && - (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { - FeNode *nonnull; - FeNode *nullnode; - nonnull=fe_m7_is_null(n->a) ? n->b : n->a; - nullnode=fe_m7_is_null(n->a) ? n->a : n->b; - a=m7_check_expr(s,nonnull); - if (!a || a->kind!=FE_TYPE_OPTIONAL) - err(s->c,n->loc,"null comparison requires an optional value"); - else { - nullnode->sem_type=a; - nullnode->sem_context=a; - } - n->sem_type=fe_type_intern(&s->c->types,"bool"); - return n->sem_type; - } - a=m7_check_expr(s,n->a); - b=m7_check_expr(s,n->b); - if (strcmp(op,"and")==0 || strcmp(op,"or")==0) { - if ((known(a) && a->kind!=FE_TYPE_BOOL) || - (known(b) && b->kind!=FE_TYPE_BOOL)) - err(s->c,n->loc,"logical operator requires bool operands"); - n->sem_type=fe_type_intern(&s->c->types,"bool"); - return n->sem_type; - } - if (strcmp(op,"==")==0 || strcmp(op,"!=")==0 || - strcmp(op,"<")==0 || strcmp(op,"<=")==0 || - strcmp(op,">")==0 || strcmp(op,">=")==0) { - if (known(a) && known(b) && !fe_type_equal(a,b) && - !m7_actual_compatible(a,b,n->b) && - !m7_actual_compatible(b,a,n->a)) - err(s->c,n->loc,"comparison operands have different types"); - n->sem_type=fe_type_intern(&s->c->types,"bool"); - return n->sem_type; - } - if ((known(a) && !fe_type_is_integer(a)) || - (known(b) && !fe_type_is_integer(b)) || - (known(a) && known(b) && !fe_type_equal(a,b) && - !m7_actual_compatible(a,b,n->b) && - !m7_actual_compatible(b,a,n->a))) - err(s->c,n->loc,"arithmetic operands must have the same integer type"); - n->sem_type=a; - return a; - } - if (n->kind==FE_N_TYPE && n->text && strcmp(n->text,"as")==0) - return check_expr(s,n); - if (n->kind==FE_N_STRUCT_INIT) - return check_struct_init(s,n); - if (n->kind==FE_N_ARRAY_INIT) - return check_array_init(s,n); - return check_expr(s,n); -} - -static FeType *m7_check_lvalue(FeCheckerState *s, FeNode *n, int read) -{ - FeType *base; - FeFieldType *field; - FeType *owner; - if (!n) return unknown(s->c); - if (n->kind==FE_N_MEMBER) { - base=m7_check_expr(s,n->a); - if (base && base->kind==FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"optional value must be projected with '.?' first"); - return unknown(s->c); - } - if (base && base->kind==FE_TYPE_REF && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - if (!base->ref_mut) - err(s->c,n->loc,"cannot write through shared reference"); - n->sem_type=base->elem; - return base->elem; - } - owner=base; - if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && - base->elem && base->elem->kind==FE_TYPE_STRUCT) - owner=base->elem; - if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { - if (base->kind==FE_TYPE_REF && !base->ref_mut) - err(s->c,n->loc,"cannot write through shared reference"); - field=fe_type_field(owner,n->b->text); - if (!field) { - err(s->c,n->loc,"assignment requires a valid struct field"); - return unknown(s->c); - } - n->sem_type=field->type; - return field->type; - } - } - return check_lvalue(s,n,read); -} - -static FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, - FeNode *source, int *borrow_mut) -{ - FeSym *root; - int mutable; - *borrow_mut=0; - if (fe_own_is_copy_type(payload)) return payload; - root=own_root_symbol(s,source); - mutable=root && root->mutable; - *borrow_mut=mutable; - if (payload->kind==FE_TYPE_OWNED && payload->elem) - return fe_type_ref(&s->c->types,payload->elem,mutable); - return fe_type_ref(&s->c->types,payload,mutable); -} - -static void m7_check_if_let(FeCheckerState *s, FeNode *n) -{ - FeType *opt; - FeType *binding_type; - FeNode *binding; - FeSym *root; - FeScope *old; - FeFlowSlot base[FE_M7_FLOW_CAP]; - FeFlowSlot left[FE_M7_FLOW_CAP]; - FeFlowSlot right[FE_M7_FLOW_CAP]; - FeOwnState *own_base; - FeOwnState *own_left; - FeOwnState *own_right; - FeFlowBorrow *borrow_base; - FeFlowBorrow *borrow_left; - FeFlowBorrow *borrow_right; - unsigned count; - unsigned i; - int borrow_mut; - int is_some; - opt=m7_check_expr(s,n->a); - if (!opt || opt->kind!=FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"if let Some/None requires an optional value"); - return; - } - is_some=n->aux_text && strcmp(n->aux_text,"Some")==0; - if (!is_some && (!n->aux_text || strcmp(n->aux_text,"None")!=0)) - err(s->c,n->loc,"if let optional pattern must be Some or None"); - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - old=s->scope; - s->scope=scope_new(s,old); - root=0; - borrow_mut=0; - binding=n->children; - if (is_some && binding) { - binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); - add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, - local_cname(s->c,binding->text),binding); - if (!fe_own_is_copy_type(opt->elem)) { - root=own_root_symbol(s,n->a); - if (root) - fe_own_access(s->c->diags,&root->own, - borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, - n->loc); - } - } - m7_check_stmt(s,n->b); - if (root) { - if (borrow_mut) fe_own_release_exclusive(&root->own); - else fe_own_release_shared(&root->own); - } - s->scope=old; - flow_capture(s->scope,left,count); - own_left=flow_own_new(s,count); - borrow_left=flow_borrow_new(s,count); - flow_own_capture(left,own_left,count); - flow_borrow_capture(left,borrow_left,count); - m7_restore_flow(base,own_base,borrow_base,count); - if (n->c) m7_check_stmt(s,n->c); - if (n->c) { - flow_capture(s->scope,right,count); - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - flow_own_capture(right,own_right,count); - flow_borrow_capture(right,borrow_right,count); - } else { - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - for (i=0;ichildren;arm;arm=arm->next) { - m7_restore_flow(base,own_base,borrow_base,count); - old=s->scope; - s->scope=scope_new(s,old); - root=0; - borrow_mut=0; - if (arm->text && strcmp(arm->text,"Some")==0) { - if (seen_some) err(s->c,arm->loc,"duplicate Some match arm"); - seen_some=1; - binding=arm->children; - if (binding) { - binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); - add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, - local_cname(s->c,binding->text),binding); - if (!fe_own_is_copy_type(opt->elem)) { - root=own_root_symbol(s,n->a); - if (root) - fe_own_access(s->c->diags,&root->own, - borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, - arm->loc); - } - } - } else if (arm->text && strcmp(arm->text,"None")==0) { - if (seen_none) err(s->c,arm->loc,"duplicate None match arm"); - seen_none=1; - } else if (arm->text && strcmp(arm->text,"_")==0) { - wildcard=1; - } else { - err(s->c,arm->loc,"optional match arm must be Some, None, or _"); - } - if (arm->a && arm->a->kind==FE_N_BLOCK) m7_check_stmt(s,arm->a); - else if (arm->a) m7_check_expr(s,arm->a); - if (root) { - if (borrow_mut) fe_own_release_exclusive(&root->own); - else fe_own_release_shared(&root->own); - } - s->scope=old; - flow_capture(s->scope,current,count); - own_current=flow_own_new(s,count); - borrow_current=flow_borrow_new(s,count); - flow_own_capture(current,own_current,count); - flow_borrow_capture(current,borrow_current,count); - if (!have) { - for (i=0;ic,n->loc,"non-exhaustive optional match"); - if (have) m7_restore_flow(merged,own_merged,borrow_merged,count); -} - -static void m7_check_match_stmt(FeCheckerState *s, FeNode *n) -{ - FeType *value; - value=m7_check_expr(s,n->a); - if (value && value->kind==FE_TYPE_OPTIONAL) { - m7_check_optional_match(s,n,value); - n->sem_type=unknown(s->c); - return; - } - check_match(s,n); -} - -static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) -{ - FeType *expected; - FeType *actual; - FeType *stored; - FeSym *sym; - int initialized; - expected=n->a ? node_type(s->c,n->a) : 0; - if (n->b) - stored=m7_check_expected(s,n->b,expected); - else - stored=expected ? expected : unknown(s->c); - actual=n->b && n->b->sem_type ? n->b->sem_type : stored; - if (!expected) expected=stored; - if (!n->a && n->b && fe_m7_is_null(n->b)) - err(s->c,n->loc,"null initializer requires an explicit optional type"); - if (expected && expected->kind==FE_TYPE_VOID) - err(s->c,n->loc,"variable cannot have void type"); - if (n->b && !fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->b)) - err(s->c,n->loc,"initializer type mismatch"); - if (n->b) mark_moved(s,n->b,actual); - initialized=n->b!=0; - sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, - local_cname(s->c,n->text ? n->text : "local"),n); - if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && - (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { - sym->borrow_root=own_root_symbol(s,n->b->a); - sym->borrow_mut=strcmp(n->b->text,"&mut")==0; - sym->borrow_defer=s->defer_depth!=0 || - own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); - } - own_bind_derived_call(s,sym,n->b); -} - -static void m7_check_stmt(FeCheckerState *s, FeNode *n) -{ - FeScope *old; - FeNode *x; - FeType *expected; - FeType *actual; - FeType *stored; - FeSym *sym; - if (!n) return; - switch (n->kind) { - case FE_N_BLOCK: - old=s->scope; - s->scope=scope_new(s,old); - for (x=n->children;x;x=x->next) { - m7_check_stmt(s,x); - own_release_after_stmt(s,s->scope,x,0); - } - own_release_after_stmt(s,s->scope,n,1); - s->scope=old; - break; - case FE_N_LET: - case FE_N_CONST: - m7_check_decl_stmt(s,n,0); - break; - case FE_N_VAR: - if (!n->a && !n->b) - err(s->c,n->loc,"uninitialized var requires an explicit type"); - m7_check_decl_stmt(s,n,1); - break; - case FE_N_ASSIGN: - expected=m7_check_lvalue(s,n->a,compound_operator(n->text)); - stored=m7_check_expected(s,n->b,expected); - actual=n->b && n->b->sem_type ? n->b->sem_type : stored; - if (!fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->b)) - err(s->c,n->loc,"assignment type mismatch"); - mark_moved(s,n->b,actual); - sym=n->a && n->a->kind==FE_N_IDENT ? - find_symbol(s->scope,n->a->text) : 0; - if (sym && sym->mutable) { - sym->initialized=1; - fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); - sym->moved=sym->own.move; - } - break; - case FE_N_EXPR_STMT: - m7_check_expr(s,n->a); - break; - case FE_N_DEFER: - ++s->defer_depth; - m7_check_stmt(s,n->a); - --s->defer_depth; - break; - case FE_N_IF: - if (n->text && strcmp(n->text,"if let")==0) - m7_check_if_let(s,n); - else { - FeType *cond; - FeFlowSlot base[FE_M7_FLOW_CAP]; - FeFlowSlot left[FE_M7_FLOW_CAP]; - FeFlowSlot right[FE_M7_FLOW_CAP]; - FeOwnState *own_base; - FeOwnState *own_left; - FeOwnState *own_right; - FeFlowBorrow *borrow_base; - FeFlowBorrow *borrow_left; - FeFlowBorrow *borrow_right; - unsigned count; - unsigned i; - cond=m7_check_expr(s,n->a); - if (known(cond) && cond->kind!=FE_TYPE_BOOL) - err(s->c,n->loc,"if condition must be bool"); - /* Once a function is on the M7 checker path, branch bodies must - remain on that path as well. In particular, return T inside - E!T relies on contextual success construction even when the - branch itself contains no surface M7 syntax. */ - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - m7_check_stmt(s,n->b); - flow_capture(s->scope,left,count); - own_left=flow_own_new(s,count); - borrow_left=flow_borrow_new(s,count); - flow_own_capture(left,own_left,count); - flow_borrow_capture(left,borrow_left,count); - m7_restore_flow(base,own_base,borrow_base,count); - if (n->c) m7_check_stmt(s,n->c); - if (n->c) { - flow_capture(s->scope,right,count); - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - flow_own_capture(right,own_right,count); - flow_borrow_capture(right,borrow_right,count); - } else { - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - for (i=0;iret; - if (n->a) - stored=m7_check_expected(s,n->a,expected); - else - stored=fe_type_intern(&s->c->types,"void"); - actual=n->a && n->a->sem_type ? n->a->sem_type : stored; - if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a && - actual && actual->kind==FE_TYPE_ERROR_UNION && - !fe_type_equal(expected,actual)) - err(s->c,n->loc,"error result type mismatch"); - else if (!fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->a)) - err(s->c,n->loc,"return type mismatch"); - if (n->a) mark_moved(s,n->a,actual); - break; - case FE_N_WHILE: - case FE_N_FOR: - /* M7 fixtures only need existing loop semantics; any M7 expression in - a loop is still recursively checked by function-local expressions - used in the body through the fallback path below. */ - if (!m7_node_feature(n)) check_stmt(s,n); - else { - if (n->kind==FE_N_WHILE) { - actual=m7_check_expr(s,n->a); - if (known(actual) && actual->kind!=FE_TYPE_BOOL) - err(s->c,n->loc,"while condition must be bool"); - ++s->loop_depth; - m7_check_stmt(s,n->b); - --s->loop_depth; - } else check_for(s,n); - } - break; - case FE_N_BREAK: - case FE_N_CONTINUE: - if (!s->loop_depth) - err(s->c,n->loc,"break or continue outside loop"); - break; - case FE_N_UNSAFE: - m7_check_stmt(s,n->a); - break; - default: - check_stmt(s,n); - break; - } -} - -static int m7_ast_reference_storage(FeNode *type) -{ - if (!type || !type->text) return 0; - if (strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || - (strcmp(type->text,"[")==0 && !type->a) || - strcmp(type->text,"str")==0) - return 1; - if (strcmp(type->text,"?")==0) - return m7_ast_reference_storage(type->a); - if (strcmp(type->text,"^")==0) return 0; - return 0; -} - -static void m7_check_storage(FeCheck *c, FeNode *decl) -{ - FeNode *m; - if (!decl) return; - if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { - for (m=decl->children;m;m=m->next) - if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a)) - err(c,m->loc,"reference type is not allowed in aggregate storage"); - } - check_reference_storage(c,decl); -} - -static void m7_validate_error_decl(FeCheck *c, FeNode *decl) -{ - FeNode *a; - FeNode *b; - unsigned long code; - unsigned long other; - if (!decl || decl->kind!=FE_N_ERROR_DECL) return; - for (a=decl->children;a;a=a->next) { - if (!a->a || a->a->kind!=FE_N_LITERAL || !a->a->text) continue; - code=strtoul(a->a->text,0,0); - if (code==0UL) - err(c,a->loc,"error code 0 is reserved for success"); - for (b=decl->children;b && b!=a;b=b->next) { - if (a->text && b->text && strcmp(a->text,b->text)==0) { - err(c,a->loc,"duplicate error member name"); - break; - } - if (b->a && b->a->kind==FE_N_LITERAL && b->a->text) { - other=strtoul(b->a->text,0,0); - if (other==code) { - err(c,a->loc,"duplicate error numeric code"); - break; - } - } - } - } -} - -static void m7_check_fn(FeCheck *c, FeNode *fn, FeScope *globals) -{ - FeCheckerState s; - FeNode *x; - FeType *t; - s.c=c; - s.globals=globals; - s.scope=scope_new(&s,globals); - s.ret=fn->b ? node_type(c,fn->b) : fe_type_intern(&c->types,"void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=fn; - fe_own_liveness_init(&s.liveness,&c->ast->arena); - fe_own_collect_last_uses(&s.liveness,fn); - fn->sem_type=s.ret; - for (x=fn->a ? fn->a->children : 0;x;x=x->next) { - t=node_type(c,x->a); - if (t->kind==FE_TYPE_VOID) - err(c,x->loc,"parameter cannot have void type"); - add_symbol(&s,s.scope,x->text,t,0,1,1, - local_cname(c,x->text ? x->text : "arg"),x); - } - if (fn->c) m7_check_stmt(&s,fn->c); -} - -static void m7_check_method(FeCheck *c, FeNode *fn, FeScope *globals, - FeType *owner) -{ - FeCheckerState s; - FeNode *x; - FeType *t; - s.c=c; - s.globals=globals; - s.scope=scope_new(&s,globals); - s.ret=fn->b ? method_type(c,fn->b,owner) : - fe_type_intern(&c->types,"void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=fn; - fe_own_liveness_init(&s.liveness,&c->ast->arena); - fe_own_collect_last_uses(&s.liveness,fn); - fn->sem_type=s.ret; - for (x=fn->a ? fn->a->children : 0;x;x=x->next) { - t=method_type(c,x->a,owner); - x->sem_type=t; - add_symbol(&s,s.scope,x->text,t,0,1,1, - local_cname(c,x->text ? x->text : "arg"),x); - } - if (fn->c) m7_check_stmt(&s,fn->c); -} - -void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, - unsigned pointer_bits, int no_checks) -{ - fe_check_init_m6(c,ast,diags,pointer_bits,no_checks); -} - -int fe_check_program(FeCheck *c) -{ - FeCheckerState s; - FeNode *n; - FeNode *m; - FeSym *sym; - FeType *t; - FeType *iv; - char method_name[128]; - if (!m7_program_feature(c)) return fe_check_program_m6(c); - s.c=c; - s.scope=scope_new(&s,0); - s.globals=s.scope; - s.ret=fe_type_intern(&c->types,"void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=0; - fe_own_liveness_init(&s.liveness,&c->ast->arena); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) - fe_type_declare_struct(&c->types,n,(n->flags & 1U)!=0); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { - m7_check_storage(c,n); - if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); - } - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_ENUM) fe_type_declare_enum(&c->types,n); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); - check_type_cycles(c); - fe_type_layout_all(&c->types); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { - if (n->kind==FE_N_STRUCT) { - for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) { - sprintf(method_name,"%s_%s",n->text ? n->text : "Type", - m->text ? m->text : "method"); - m->cname=unit_cname(c,method_name); - } - } - if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { - t=n->a ? node_type(c,n->a) : unknown(c); - add_symbol(&s,s.globals,n->text,t,0,n->kind==FE_N_GLOBAL, - n->b!=0,unit_cname(c,n->text ? n->text : "global"),n); - } - } - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) { - t=fe_type_intern(&c->types,""); - add_symbol(&s,s.globals,n->text,t,n,0,1, - unit_cname(c,n->text ? n->text : "fn"),n); - } - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { - sym=find_current(s.globals,n->text ? n->text : ""); - if (n->b) { - iv=m7_check_expected(&s,n->b,sym ? sym->type : 0); - if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { - sym->type=iv; - n->sem_type=iv; - } else if (sym && !fe_type_equal(sym->type,iv) && - !m7_actual_compatible(sym->type,iv,n->b)) - err(c,n->loc,"global initializer type mismatch"); - } - } - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) m7_check_fn(c,n,s.globals); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) { - t=fe_type_intern(&c->types,n->text); - for (m=n->children;m;m=m->next) - if (m->kind==FE_N_FN) m7_check_method(c,m,s.globals,t); - } - fe_type_layout_all(&c->types); - return c->diags->errors==0; -} - -FeType *fe_check_expr_type(FeCheck *c, FeNode *n) -{ - FeCheckerState s; - s.c=c; - s.scope=scope_new(&s,0); - s.globals=s.scope; - s.ret=fe_type_intern(&c->types,"void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=0; - fe_own_liveness_init(&s.liveness,&c->ast->arena); - if (m7_node_feature(n)) return m7_check_expr(&s,n); - return fe_check_expr_type_m6(c,n); -} From 3b8c8ac67468d64ca1e2dfc64c93b46d5ed49cd7 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:49:02 +0900 Subject: [PATCH 104/184] refactor: unify M7 lowering and C emission emitcm7.c was the same wrapper trick as the checker: nineteen #define renames, a textual include of emit_c.c, and a per-unit feature scan choosing between two emitters. Five of the M7 halves fell through to their M6 counterpart, so those become the single entry point with the old body renamed to *_core; the other thirteen never delegated at all and simply replace the M6 version. The scan is gone from both places it was used -- the program entry and the expression emitter, whose switch already ended in a default that delegates. fe_emit_c_program_core went with the dispatch that was its only caller. emitcm7.c is deleted and the build compiles emit_c.c directly. Nothing in the compiler now selects an implementation by looking for `?` or `!` in a unit. This is the pair to the checker commit; together they end the two-engine split that produced four of the five defects fixed while getting M7 to pass. --- fec/Makefile | 2 +- fec/build-dos.bat | 4 +- fec/src/emit_c.c | 1632 ++++++++++++++++++++++++++++++++++++--------- fec/src/emitcm7.c | 1420 --------------------------------------- 4 files changed, 1325 insertions(+), 1733 deletions(-) delete mode 100644 fec/src/emitcm7.c diff --git a/fec/Makefile b/fec/Makefile index 92e678b..a6eb4c5 100644 --- a/fec/Makefile +++ b/fec/Makefile @@ -1,7 +1,7 @@ CC ?= cc CFLAGS ?= -O2 -Wall -Wextra -std=c89 CPPFLAGS ?= -Isrc -SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check.c src/lower.c src/emitcm7.c src/driver.c +SRC = src/arena.c src/diag.c src/lexer.c src/ast.c src/parser.c src/types.c src/m7.c src/own.c src/check.c src/lower.c src/emit_c.c src/driver.c OBJ = $(SRC:.c=.o) .PHONY: all clean dos-build diff --git a/fec/build-dos.bat b/fec/build-dos.bat index 627336d..733cbea 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -31,8 +31,8 @@ wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lower.obj src\lower.c if errorlevel 1 goto build_fail -rem Use an unambiguous short object name for the M7 emitter source. -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emitcm7.c +rem Use an unambiguous short object name for the emitter source. +wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emit_c.c if errorlevel 1 goto build_fail wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=driver.obj src\driver.c if errorlevel 1 goto build_fail diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 6d44e33..53c37d7 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -2,8 +2,8 @@ #include #include -static void emit_expr(FeEmitter *e, FeNode *n); -static void emit_stmt(FeEmitter *e, FeNode *n); +static void emit_expr_core(FeEmitter *e, FeNode *n); +static void emit_stmt_core(FeEmitter *e, FeNode *n); static void emit_block(FeEmitter *e, FeNode *n); static void pad(FeEmitter *e) @@ -28,18 +28,6 @@ static const char *cname(FeNode *n, const char *fallback) static void emit_one_type(FeEmitter *e, FeType *t); -static int type_needs_drop(FeType *t) -{ - unsigned i; - if (!t) return 0; - if (t->kind==FE_TYPE_OWNED) return 1; - if (t->kind==FE_TYPE_ARRAY) return type_needs_drop(t->elem); - if (t->kind==FE_TYPE_STRUCT) { - if (t->has_drop) return 1; - for (i=0;ifield_count;i++) if (type_needs_drop(t->fields[i].type)) return 1; - } - return 0; -} static void emit_type_deps(FeEmitter *e, FeType *t) { @@ -107,16 +95,6 @@ static void emit_one_type(FeEmitter *e, FeType *t) t->emit_state=2; } -static void emit_type_defs(FeEmitter *e) -{ - FeType *t; - /* Every fixed array has a slice conversion helper, even if this unit - only indexes the array. Intern those result types before emission so - their typedefs are present before helper definitions. */ - for(t=e->check->types.types;t;t=t->next) - if(t->kind==FE_TYPE_ARRAY) fe_type_slice(&e->check->types,t->elem); - for(t=e->check->types.types;t;t=t->next) emit_one_type(e,t); -} static FeNode *find_drop_method(FeEmitter *e, const char *name) { @@ -348,6 +326,15 @@ static int node_uses_m4(FeNode *n) static void emit_expr(FeEmitter *e, FeNode *n); static void emit_stmt(FeEmitter *e, FeNode *n); +static void emit_value_drop(FeEmitter *e, FeNode *n); +static void emit_cleanup_block(FeEmitter *e, FeNode *n); +static void emit_cleanup_to(FeEmitter *e, unsigned depth); +static void emit_param_cleanup(FeEmitter *e); +static void emit_error_return(FeEmitter *e, FeNode *n); +static void emit_fn(FeEmitter *e, FeNode *fn, int prototype); +static void emit_main_wrapper(FeEmitter *e, FeNode *fn); +static void emit_type_defs(FeEmitter *e); +static void emit_match(FeEmitter *e, FeNode *n, int value_context); static int stmt_definitely_returns(FeNode *n); @@ -645,32 +632,6 @@ static void emit_m4_builtin(FeEmitter *e, FeNode *n) (void)count; } -static void emit_lvalue(FeEmitter *e, FeNode *n) -{ - FeType *bt; - if (!n) { fputs("fe_bad_lvalue",e->out); return; } - if (n->kind==FE_N_IDENT) { fputs(cname(n,"fe_local"),e->out); return; } - if (n->kind==FE_N_MEMBER) { - if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); - } else if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OWNED && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); - } else if(n->a && n->a->sem_type && - n->a->sem_type->kind==FE_TYPE_REF) { - emit_expr(e,n->a); fputs("->",e->out); - fputs(n->b ? n->b->text : "member",e->out); - } else { emit_lvalue(e,n->a); fputc('.',e->out); fputs(n->b ? n->b->text : "member",e->out); } - return; - } - if (n->kind==FE_N_INDEX) { - bt=n->a ? n->a->sem_type : 0; - emit_lvalue(e,n->a); fputs(bt && bt->kind==FE_TYPE_ARRAY ? ".a[" : ".p[",e->out); - emit_expr(e,n->b); fputc(']',e->out); return; - } - emit_expr(e,n); -} static void emit_destroy_expr(FeEmitter *e, FeNode *n) { @@ -757,7 +718,7 @@ static void emit_slice(FeEmitter *e, FeNode *n) emit_slice_call(e,n); } -static void emit_expr(FeEmitter *e, FeNode *n) +static void emit_expr_core(FeEmitter *e, FeNode *n) { FeNode *x; const char *op; @@ -996,181 +957,16 @@ static void emit_expr(FeEmitter *e, FeNode *n) } } -static void emit_decl(FeEmitter *e, FeNode *n) -{ - pad(e); - fputs(ctype(e, n), e->out); - fputc(' ', e->out); - fputs(cname(n, "fe_local"), e->out); - if (n->kind==FE_N_CONST && n->b) { - fputs(" = ",e->out); - if (n->b->kind==FE_N_LITERAL && n->b->text && n->b->text[0]=='"') { - fputs("{ (const unsigned char*)",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(", sizeof(",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(")-1 }",e->out); - } else emit_expr(e,n->b); - } - fputs(";\n", e->out); - if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && - type_needs_drop(n->sem_type)) { - pad(e); fputs("unsigned char fe_live_",e->out); - fputs(cname(n,"owned"),e->out); fputs("=0;\n",e->out); - } -} -static void emit_owned_live(FeEmitter *e, FeNode *n, int value) -{ - if (n && n->sem_type && type_needs_drop(n->sem_type)) { - pad(e); fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fprintf(e->out,"=%d;\n",value); - } -} -static void emit_value_drop(FeEmitter *e, FeNode *n) -{ - FeType *t=n ? n->sem_type : 0; - if (!n || !t || !type_needs_drop(t) || (n->flags & 0x100U) || - (n->flags & 0x200U)) return; - if (t->kind==FE_TYPE_OWNED) { - pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs(") { ",e->out); - if(t->elem && t->elem->kind==FE_TYPE_SLICE) { - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); - fputs(".p); ",e->out); - } else if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) { - fprintf(e->out,"%s(%s); ",t->elem->drop_cname,cname(n,"owned")); - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); fputs("); ",e->out); - } else { - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); fputs("); ",e->out); - } - fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs("=0; }\n",e->out); - } else if (t->drop_cname) { - pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"local"),e->out); - fprintf(e->out,") { %s(&%s); fe_live_",t->drop_cname,cname(n,"local")); - fputs(cname(n,"local"),e->out); fputs("=0; }\n",e->out); - } -} -static void emit_cleanup_block(FeEmitter *e, FeNode *n) -{ - FeNode *x; - unsigned count=0; - unsigned index; - unsigned seen=0xffffffffU; - unsigned depth; - /* A defer becomes active only after its declaration statement was - reached. In particular, a failing initializer must not run a later - defer merely because it shares this AST block. */ - for (depth=0; depthblock_depth; ++depth) - if (e->block_stack[depth]==n) { seen=e->block_seen[depth]; break; } - for (x=n ? n->children : 0, index=0; x; x=x->next, ++index) - if (indexkind==FE_N_DEFER || x->kind==FE_N_LET || x->kind==FE_N_VAR)) ++count; - while (count) { - index=0; - for (x=n->children; x; x=x->next) - if ((x->kind==FE_N_DEFER || x->kind==FE_N_LET || x->kind==FE_N_VAR) && - index++==count-1) { - if (x->kind==FE_N_DEFER) emit_stmt(e,x->a); else emit_value_drop(e,x); - break; - } - --count; - } -} -static void emit_cleanup_to(FeEmitter *e, unsigned floor) -{ - unsigned i; - for (i=e->block_depth; i>floor; --i) emit_cleanup_block(e,e->block_stack[i-1]); -} -static void emit_param_cleanup(FeEmitter *e) -{ - FeNode *p; - if (!e->current_fn || !e->current_fn->a) return; - for (p=e->current_fn->a->children; p; p=p->next) emit_value_drop(e,p); -} -static void emit_cleanup_all(FeEmitter *e) -{ - emit_cleanup_to(e,0); - emit_param_cleanup(e); -} -static void emit_error_return(FeEmitter *e, const char *error_expr) -{ - if (e->current_ret && e->current_ret->kind==FE_TYPE_ERROR_UNION && - e->current_ret->error_value && e->current_ret->error_value->kind!=FE_TYPE_VOID) { - fputs("return ",e->out); fputs(e->current_ret->maker,e->out); - fputs("(",e->out); fputs(error_expr,e->out); fputs(", (",e->out); - fputs(fe_type_c_name(e->current_ret->error_value,e->pointer_bits),e->out); - fputs(")0);\n",e->out); - } else { - fputs("return ",e->out); fputs(error_expr,e->out); fputs(";\n",e->out); - } -} -static void emit_block(FeEmitter *e, FeNode *n) -{ - FeNode *x; - if (!n) { - pad(e); - fputs("{\n", e->out); - ++e->indent; - --e->indent; - pad(e); - fputc('}', e->out); - return; - } - pad(e); - fputs("{\n", e->out); - ++e->indent; - if (e->block_depth<32U) { - e->block_stack[e->block_depth]=n; - e->block_seen[e->block_depth]=0; - ++e->block_depth; - } - /* C89 requires declarations before statements in each actual block. */ - for (x = n->children; x; x = x->next) - if (x->kind == FE_N_LET || x->kind == FE_N_VAR || - x->kind == FE_N_CONST) emit_decl(e, x); - if (e->current_fn && e->current_fn->c==n && e->current_fn->a) { - FeNode *param; - for (param=e->current_fn->a->children; param; param=param->next) - if (param->sem_type && type_needs_drop(param->sem_type)) { - pad(e); fputs("unsigned char fe_live_",e->out); - fputs(cname(param,"owned"),e->out); fputs("=1;\n",e->out); - } - } - if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs(fe_type_c_name(e->current_ret,e->pointer_bits),e->out); - fputs(" fe_return_value;\n",e->out); - } - { - unsigned seen=0; - for (x = n->children; x; x = x->next) { - ++seen; - if (e->block_depth) e->block_seen[e->block_depth-1]=seen; - emit_stmt(e, x); - } - } - --e->indent; - emit_cleanup_block(e,n); - if (e->current_fn && e->current_fn->c==n) emit_param_cleanup(e); - if (e->block_depth) --e->block_depth; - if (e->fallthrough_block==n) { - pad(e); - fputs("return 0;\n",e->out); - e->fallthrough_block=0; - } - pad(e); - fputc('}', e->out); -} -static void emit_match(FeEmitter *e, FeNode *n, int value_context) +static void emit_match_core(FeEmitter *e, FeNode *n, int value_context) { FeNode *arm; FeType *t=n->a ? n->a->sem_type : 0; @@ -1233,7 +1029,7 @@ static void emit_try_statement(FeEmitter *e, FeNode *try_node, FeNode *target) fputs("}\n",e->out); } -static void emit_stmt(FeEmitter *e, FeNode *n) +static void emit_stmt_core(FeEmitter *e, FeNode *n) { if (!n) return; switch (n->kind) { @@ -1409,46 +1205,8 @@ static void emit_stmt(FeEmitter *e, FeNode *n) } } -static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) -{ - FeNode *p; - const char *ret; - ret = fn->sem_type ? fe_type_c_name(fn->sem_type, e->pointer_bits) : - (fn->b ? ctype(e, fn->b) : "void"); - fputs(ret, e->out); - fputc(' ', e->out); - fputs(cname(fn, "fe_fn"), e->out); - fputc('(', e->out); - p = fn->a ? fn->a->children : 0; - if (!p) fputs("void", e->out); - while (p) { - if (p != fn->a->children) fputs(", ", e->out); - fputs(p->sem_type ? fe_type_c_name(p->sem_type,e->pointer_bits) : - ctype(e,p->a),e->out); - fputc(' ', e->out); - fputs(cname(p, "fe_arg"), e->out); - p = p->next; - } - fputc(')', e->out); - if (prototype) fputs(";\n", e->out); - else { - FeType *old_ret=e->current_ret; - FeNode *old_fn=e->current_fn; - e->current_ret=fn->sem_type; - e->current_fn=fn; - fputs(" ", e->out); - if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && - fn->sem_type->error_value && - fn->sem_type->error_value->kind==FE_TYPE_VOID) - e->fallthrough_block=fn->c; - emit_block(e, fn->c); - e->current_ret=old_ret; - e->current_fn=old_fn; - fputc('\n', e->out); - } -} -static void emit_main_wrapper(FeEmitter *e, FeNode *fn) +static void emit_main_wrapper_core(FeEmitter *e, FeNode *fn) { fputs("int main(void) {\n ", e->out); if (fn->sem_type && fn->sem_type->kind == FE_TYPE_VOID) { @@ -1478,75 +1236,1329 @@ void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, e->current_fn = 0; } + +#include "m7.h" +#include "lower.h" + +static void emit_expr(FeEmitter *e, FeNode *n); +static void emit_stmt(FeEmitter *e, FeNode *n); +static void emit_block(FeEmitter *e, FeNode *n); +static void emit_lvalue(FeEmitter *e, FeNode *n); + +static int type_needs_drop(FeType *t) +{ + return fe_lower_type_needs_drop(t); +} + +static const char *m7_c_type(FeEmitter *e, FeType *t) +{ + if (!t) return "long"; + if ((t->kind==FE_TYPE_ENUM && t->is_error) || + strcmp(t->name,"core.Error")==0) + return "unsigned short"; + return fe_type_c_name(t,e->pointer_bits); +} + +static char *m7_temp_name(FeEmitter *e) +{ + char number[24]; + char *p; + unsigned long len; + sprintf(number,"%u",e->temp_serial++); + len=(unsigned long)strlen("fe_m7_tmp_")+ + (unsigned long)strlen(number)+1UL; + p=(char *)fe_arena_alloc(&e->check->ast->arena,len); + if (!p) return 0; + strcpy(p,"fe_m7_tmp_"); + strcat(p,number); + return p; +} + +static int m7_needs_temp(FeNode *n) +{ + if (!n) return 0; + if (fe_m7_is_try(n)) return 1; + if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) + return 1; + if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) + return 1; + if (n->kind==FE_N_MATCH && n->a && n->a->sem_type && + n->a->sem_type->kind==FE_TYPE_OPTIONAL) + return 1; + return 0; +} + +static FeType *m7_temp_type(FeNode *n) +{ + if (!n) return 0; + if (fe_m7_is_try(n)) return n->a ? n->a->sem_type : 0; + if (n->kind==FE_N_BINARY) return n->a ? n->a->sem_type : 0; + if ((n->kind==FE_N_IF || n->kind==FE_N_MATCH) && n->a) + return n->a->sem_type; + return 0; +} + +static void m7_prepare_temps(FeEmitter *e, FeNode *n) +{ + FeNode *x; + if (!n) return; + if (m7_needs_temp(n) && !n->aux_cname) + n->aux_cname=m7_temp_name(e); + m7_prepare_temps(e,n->a); + m7_prepare_temps(e,n->b); + m7_prepare_temps(e,n->c); + for (x=n->children;x;x=x->next) m7_prepare_temps(e,x); +} + +static void m7_emit_temp_decls(FeEmitter *e, FeNode *n) +{ + FeNode *x; + FeType *t; + if (!n) return; + if (m7_needs_temp(n) && n->aux_cname) { + t=m7_temp_type(n); + if (t) { + pad(e); fputs(m7_c_type(e,t),e->out); fputc(' ',e->out); + fputs(n->aux_cname,e->out); fputs(";\n",e->out); + } + } + m7_emit_temp_decls(e,n->a); + m7_emit_temp_decls(e,n->b); + m7_emit_temp_decls(e,n->c); + for (x=n->children;x;x=x->next) m7_emit_temp_decls(e,x); +} + +static void m7_emit_type(FeEmitter *e, FeType *t) +{ + unsigned i; + unsigned j; + if (!t || t->emit_state) return; + if (t->kind==FE_TYPE_OPTIONAL) { + t->emit_state=1; + m7_emit_type(e,t->elem); + if (!fe_m7_optional_uses_niche(t->elem) && t->cname) { + fputs(t->cname,e->out); fputs(" { unsigned char has; ",e->out); + fputs(m7_c_type(e,t->elem),e->out); + fputs(" v; };\n",e->out); + } + t->emit_state=2; + return; + } + if (t->kind==FE_TYPE_ARRAY) m7_emit_type(e,t->elem); + if (t->kind==FE_TYPE_SLICE) m7_emit_type(e,t->elem); + if (t->kind==FE_TYPE_OWNED) m7_emit_type(e,t->elem); + if (t->kind==FE_TYPE_STRUCT) + for (i=0;ifield_count;++i) m7_emit_type(e,t->fields[i].type); + if (t->kind==FE_TYPE_ENUM) + for (i=0;ivariant_count;++i) + for (j=0;jvariants[i].field_count;++j) + m7_emit_type(e,t->variants[i].fields[j].type); + if (t->kind==FE_TYPE_ERROR_UNION) { + m7_emit_type(e,t->elem); + m7_emit_type(e,t->error_value); + } + emit_one_type(e,t); +} + +static void emit_type_defs(FeEmitter *e) +{ + FeType *t; + for (t=e->check->types.types;t;t=t->next) + if (t->kind==FE_TYPE_ARRAY) + fe_type_slice(&e->check->types,t->elem); + for (t=e->check->types.types;t;t=t->next) m7_emit_type(e,t); +} + +static void m7_emit_drop_access(FeEmitter *e, FeType *t, + const char *access) +{ + if (!t || !access || !type_needs_drop(t)) return; + if (t->kind==FE_TYPE_OWNED) { + if (t->elem && t->elem->kind==FE_TYPE_SLICE) { + fprintf(e->out,"if ((%s).p) { free((%s).p); (%s).p=0; } ", + access,access,access); + } else { + fprintf(e->out,"if (%s) { ",access); + if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) + fprintf(e->out,"%s(%s); ",t->elem->drop_cname,access); + fprintf(e->out,"free(%s); %s=0; } ",access,access); + } + return; + } + if (t->drop_cname) + fprintf(e->out,"%s(&(%s)); ",t->drop_cname,access); +} + +static void m7_emit_drop_helpers(FeEmitter *e) +{ + FeType *t; + FeNode *method; + unsigned i; + unsigned j; + char access[256]; + for (t=e->check->types.types;t;t=t->next) + if (type_needs_drop(t) && t->drop_cname && + (t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY || + t->kind==FE_TYPE_OPTIONAL || t->kind==FE_TYPE_ERROR_UNION)) + fprintf(e->out,"static void %s(%s *self);\n", + t->drop_cname,m7_c_type(e,t)); + for (t=e->check->types.types;t;t=t->next) { + if (!type_needs_drop(t) || !t->drop_cname) continue; + if (t->kind==FE_TYPE_STRUCT) { + fprintf(e->out,"static void %s(%s *self) { ", + t->drop_cname,m7_c_type(e,t)); + method=find_drop_method(e,t->name); + if (method) fprintf(e->out,"%s(self); ",cname(method,"fe_drop_method")); + for (i=t->field_count;i>0;--i) { + sprintf(access,"self->%s",t->fields[i-1U].name); + m7_emit_drop_access(e,t->fields[i-1U].type,access); + } + fputs("}\n",e->out); + } else if (t->kind==FE_TYPE_ARRAY) { + fprintf(e->out,"static void %s(%s *self) { unsigned long i; for (i=0; i<%lu; ++i) { ", + t->drop_cname,m7_c_type(e,t),t->length); + strcpy(access,"self->a[i]"); + m7_emit_drop_access(e,t->elem,access); + fputs("} }\n",e->out); + } else if (t->kind==FE_TYPE_OPTIONAL) { + fprintf(e->out,"static void %s(%s *self) { ", + t->drop_cname,m7_c_type(e,t)); + if (fe_m7_optional_uses_niche(t->elem)) { + fputs("if (*self) { ",e->out); + m7_emit_drop_access(e,t->elem,"*self"); + fputs("} ",e->out); + } else { + fputs("if (self->has) { ",e->out); + m7_emit_drop_access(e,t->elem,"self->v"); + fputs("self->has=0; } ",e->out); + } + fputs("}\n",e->out); + } else if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && + t->error_value->kind!=FE_TYPE_VOID) { + fprintf(e->out,"static void %s(%s *self) { if (!self->e) { ", + t->drop_cname,m7_c_type(e,t)); + m7_emit_drop_access(e,t->error_value,"self->v"); + fputs("self->e=1; } }\n",e->out); + } + } + /* Error enums are scalar codes, so their enum payload helper functions + from M3 are deliberately not emitted in the M7 path. */ + (void)j; +} + +static void m7_emit_type_helpers(FeEmitter *e) +{ + FeType *t; + FeType *st; + unsigned i; + unsigned j; + const char *ct; + FeVariantType *v; + for (t=e->check->types.types;t;t=t->next) { + if (t->kind==FE_TYPE_OPTIONAL) { + if (!fe_m7_optional_uses_niche(t->elem)) { + fprintf(e->out,"static %s %s(%s v) { %s r; r.has=1; r.v=v; return r; }\n", + m7_c_type(e,t),t->maker,m7_c_type(e,t->elem),m7_c_type(e,t)); + fprintf(e->out,"static %s %s(void) { %s r; memset(&r,0,sizeof(r)); return r; }\n", + m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); + fprintf(e->out,"static %s %s(%s x) { ", + m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); + if (!e->no_checks) fputs("if (!x.has) fe_trap_bounds(); ",e->out); + fputs("return x.v; }\n",e->out); + } else { + fprintf(e->out,"static %s %s(%s x) { ", + m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); + if (!e->no_checks) fputs("if (!x) fe_trap_bounds(); ",e->out); + fputs("return x; }\n",e->out); + } + } + if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && + t->error_value->kind!=FE_TYPE_VOID) { + fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", + m7_c_type(e,t),t->maker,m7_c_type(e,t->error_value),m7_c_type(e,t)); + if (t->none_cname) + fprintf(e->out,"static %s %s(unsigned short e) { %s r; memset(&r,0,sizeof(r)); r.e=e; return r; }\n", + m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); + if (t->error_value->kind==FE_TYPE_OWNED && + t->error_value->elem && t->error_value->elem->kind==FE_TYPE_SLICE) { + FeType *item=t->error_value->elem->elem; + fprintf(e->out,"static %s %s(unsigned long n) { %s r; r.v.p=(%s*)malloc(sizeof(%s)*n); r.v.n=n; r.e=(r.v.p || !n) ? 0 : 1; return r; }\n", + m7_c_type(e,t),t->alloc_cname,m7_c_type(e,t), + m7_c_type(e,item),m7_c_type(e,item)); + } else if (t->error_value->kind==FE_TYPE_OWNED) { + fprintf(e->out,"static %s %s(%s v) { %s r; r.v=(%s)malloc(sizeof(%s)); if(r.v) *r.v=v; r.e=r.v ? 0 : 1; return r; }\n", + m7_c_type(e,t),t->alloc_cname, + m7_c_type(e,t->error_value->elem),m7_c_type(e,t), + m7_c_type(e,t->error_value), + m7_c_type(e,t->error_value->elem)); + } + } + } + for (t=e->check->types.types;t;t=t->next) { + if (t->replace_cname) { + ct=m7_c_type(e,t); + fprintf(e->out,"static %s %s(%s *dst, %s value) { %s old=*dst; *dst=value; return old; }\n", + ct,t->replace_cname,ct,ct,ct); + } + if (t->kind==FE_TYPE_STRUCT && t->maker) { + fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); + for (i=0;ifield_count;i++) { + if (i) fputs(", ",e->out); + fputs(m7_c_type(e,t->fields[i].type),e->out); + fprintf(e->out," p%u",i); + } + fprintf(e->out,") { %s v;",m7_c_type(e,t)); + for (i=0;ifield_count;i++) + fprintf(e->out," v.%s=p%u;",t->fields[i].name,i); + fputs(" return v; }\n",e->out); + } else if (t->kind==FE_TYPE_ARRAY && t->maker) { + fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); + for (i=0;ilength;i++) { + if (i) fputs(", ",e->out); + fputs(m7_c_type(e,t->elem),e->out); + fprintf(e->out," p%u",i); + } + fprintf(e->out,") { %s v;",m7_c_type(e,t)); + for (i=0;ilength;i++) fprintf(e->out," v.a[%u]=p%u;",i,i); + fputs(" return v; }\n",e->out); + } else if (t->kind==FE_TYPE_ENUM && !t->is_error) { + for (i=0;ivariant_count;i++) { + v=&t->variants[i]; + fprintf(e->out,"static %s %s(",m7_c_type(e,t),v->maker); + for (j=0;jfield_count;j++) { + if (j) fputs(", ",e->out); + fputs(m7_c_type(e,v->fields[j].type),e->out); + fprintf(e->out," p%u",j); + } + fprintf(e->out,") { %s x; x.tag=%u;",m7_c_type(e,t),v->tag); + for (j=0;jfield_count;j++) { + if (v->field_count==1) + fprintf(e->out," x.payload.%s=p%u;",v->name,j); + else + fprintf(e->out," x.payload.%s.%s=p%u;",v->name, + v->fields[j].name,j); + } + fputs(" return x; }\n",e->out); + } + } + } + m7_emit_drop_helpers(e); + /* Reuse the mature M3 index/slice helper generator. It does not depend + on M7 drop policy and all wrapper dependencies are already emitted. */ + for (t=e->check->types.types;t;t=t->next) { + if (t->kind==FE_TYPE_ARRAY && t->indexer) { + fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", + m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); + if (!e->no_checks) + fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); + fputs("return x.a[i]; }\n",e->out); + if (t->slicer) { + st=fe_type_slice(&e->check->types,t->elem); + fprintf(e->out,"static %s %s(%s *x, unsigned long a, unsigned long b) { ", + m7_c_type(e,st),t->slicer,m7_c_type(e,t)); + if (!e->no_checks) + fprintf(e->out,"if (a > b || b > %lu) fe_trap_bounds(); ",t->length); + fprintf(e->out,"return %s(x->a+a,b-a); }\n",st->maker); + fprintf(e->out,"static %s %s(%s *x) { return %s(x,0,%lu); }\n", + m7_c_type(e,st),t->full_slicer,m7_c_type(e,t),t->slicer,t->length); + fprintf(e->out,"static %s %s(%s *x, unsigned long a) { return %s(x,a,%lu); }\n", + m7_c_type(e,st),t->tail_slicer,m7_c_type(e,t),t->slicer,t->length); + } + } else if (t->kind==FE_TYPE_SLICE && t->indexer) { + fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", + m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); + if (!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); + fputs("return x.p[i]; }\n",e->out); + if (t->slicer) { + fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", + m7_c_type(e,t),t->slicer,m7_c_type(e,t)); + if (!e->no_checks) + fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); + fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker); + fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n", + m7_c_type(e,t),t->full_slicer,m7_c_type(e,t),t->slicer); + fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n", + m7_c_type(e,t),t->tail_slicer,m7_c_type(e,t),t->slicer); + } + } + } +} + +static void m7_emit_present(FeEmitter *e, FeType *opt, const char *name) +{ + if (fe_m7_optional_uses_niche(opt->elem)) { + fputs("(",e->out); fputs(name,e->out); fputs(" != 0)",e->out); + } else { + fputs(name,e->out); fputs(".has",e->out); + } +} + +static void m7_emit_payload_var(FeEmitter *e, FeType *opt, const char *name) +{ + (void)e; + fputs(name,e->out); + if (!fe_m7_optional_uses_niche(opt->elem)) fputs(".v",e->out); +} + +static void m7_emit_error_member(FeEmitter *e, FeNode *n) +{ + FeVariantType *v; + FeType *t; + t=n && n->a ? n->a->sem_type : 0; + v=t && t->kind==FE_TYPE_ENUM ? + fe_type_variant(t,n->b ? n->b->text : "") : 0; + if (v) fprintf(e->out,"%u",v->tag); + else fputs("0",e->out); +} + +static void m7_emit_raw_expr(FeEmitter *e, FeNode *n); + +static void m7_emit_contextual(FeEmitter *e, FeNode *n) +{ + FeType *ctx; + FeType *actual; + FeType *error_type; + ctx=n ? n->sem_context : 0; + actual=n ? n->sem_type : 0; + if (!ctx) { m7_emit_raw_expr(e,n); return; } + if (ctx->kind==FE_TYPE_OPTIONAL) { + if (fe_m7_is_null(n)) { + if (fe_m7_optional_uses_niche(ctx->elem)) fputs("0",e->out); + else { fputs(ctx->none_cname,e->out); fputs("()",e->out); } + return; + } + if (fe_m7_optional_uses_niche(ctx->elem)) { + m7_emit_raw_expr(e,n); + } else { + fputs(ctx->maker,e->out); fputc('(',e->out); + m7_emit_raw_expr(e,n); fputc(')',e->out); + } + return; + } + if (ctx->kind==FE_TYPE_ERROR_UNION) { + error_type=ctx->elem; + if (!error_type) error_type=fe_type_intern(&e->check->types,"core.Error"); + if (actual && fe_type_equal(actual,ctx->error_value)) { + if (ctx->error_value->kind==FE_TYPE_VOID) fputs("0",e->out); + else { + fputs(ctx->maker,e->out); fputs("(0, ",e->out); + m7_emit_raw_expr(e,n); fputc(')',e->out); + } + return; + } + if (actual && fe_type_equal(actual,error_type)) { + if (ctx->error_value->kind==FE_TYPE_VOID) + m7_emit_raw_expr(e,n); + else { + fputs(ctx->none_cname,e->out); fputc('(',e->out); + m7_emit_raw_expr(e,n); fputc(')',e->out); + } + return; + } + } + m7_emit_raw_expr(e,n); +} + +static void emit_expr(FeEmitter *e, FeNode *n) +{ + if (!n) { fputs("0",e->out); return; } + if (n->sem_context) m7_emit_contextual(e,n); + else m7_emit_raw_expr(e,n); +} + +static void emit_lvalue(FeEmitter *e, FeNode *n) +{ + FeType *bt; + if (!n) { fputs("fe_bad_lvalue",e->out); return; } + if (n->kind==FE_N_IDENT) { + fputs(cname(n,"fe_local"),e->out); + return; + } + /* A declaration names its own storage. The initializer for `let`/`var` is + emitted as a separate assignment statement, so the declaration node is + handed here as the target; without this it falls through to the raw + expression path, which emits a declaration as "0" and produces `0 = ...`. */ + if (n->kind==FE_N_LET || n->kind==FE_N_VAR || n->kind==FE_N_CONST) { + fputs(cname(n,"fe_local"),e->out); + return; + } + if (n->kind==FE_N_MEMBER) { + bt=n->a ? n->a->sem_type : 0; + if (n->text && strcmp(n->text,".?")==0) { + emit_expr(e,n); + return; + } + if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); + } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { + emit_expr(e,n->a); fputs("->",e->out); + fputs(n->b && n->b->text ? n->b->text : "member",e->out); + } else { + emit_lvalue(e,n->a); fputc('.',e->out); + fputs(n->b && n->b->text ? n->b->text : "member",e->out); + } + return; + } + if (n->kind==FE_N_INDEX) { + bt=n->a ? n->a->sem_type : 0; + emit_lvalue(e,n->a); + fputs(bt && bt->kind==FE_TYPE_ARRAY ? ".a[" : ".p[",e->out); + emit_expr(e,n->b); fputc(']',e->out); + return; + } + m7_emit_raw_expr(e,n); +} + +static void m7_emit_call(FeEmitter *e, FeNode *n) +{ + FeNode *x; + FeNode *call_param; + FeVariantType *v; + int special; + call_param=0; + special=0; + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"destroy")==0 && n->children) { + FeNode *arg=n->children; + fputs("(free(",e->out); emit_expr(e,arg); + if (arg->sem_type && arg->sem_type->kind==FE_TYPE_OWNED && + arg->sem_type->elem && arg->sem_type->elem->kind==FE_TYPE_SLICE) + fputs(".p",e->out); + fputc(')',e->out); + if (arg->kind==FE_N_IDENT) { + fputs(", ",e->out); emit_lvalue(e,arg); + if (arg->sem_type && arg->sem_type->elem && + arg->sem_type->elem->kind==FE_TYPE_SLICE) fputs(".p=0",e->out); + else fputs("=0",e->out); + fputs(", fe_live_",e->out); fputs(cname(arg,"owned"),e->out); + fputs("=0",e->out); + } + fputs(", 0)",e->out); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"replace")==0 && n->children && + n->children->next && n->sem_type) { + fputs(n->sem_type->replace_cname ? n->sem_type->replace_cname : + "fe_bad_replace",e->out); + fputc('(',e->out); emit_expr(e,n->children); fputs(", ",e->out); + emit_expr(e,n->children->next); fputc(')',e->out); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"create")==0 && n->children) { + FeType *created=n->children->sem_type; + FeType *owned=fe_type_owned(&e->check->types,created); + FeType *result=fe_type_error_union(&e->check->types,owned); + fputs(result->alloc_cname ? result->alloc_cname : "fe_bad_alloc",e->out); + fputc('(',e->out); emit_expr(e,n->children); fputc(')',e->out); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"alloc_slice")==0 && n->children && + n->children->next) { + FeType *result=n->sem_type; + fputs(result && result->alloc_cname ? result->alloc_cname : + "fe_bad_slice_alloc",e->out); + fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out); + return; + } + if (n->text && (strcmp(n->text,"@print")==0 || + strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { + emit_m4_builtin(e,n); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"null_writer")==0) { + fputs("fe_m4_null_writer()",e->out); + return; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM && + !n->a->a->sem_type->is_error) { + v=fe_type_variant(n->a->a->sem_type,n->a->b ? n->a->b->text : ""); + fputs(v ? v->maker : "fe_bad_variant",e->out); + } else if (n->a && n->a->kind==FE_N_MEMBER && n->sem_decl && + n->sem_decl->kind==FE_N_FN) { + FeNode *mp=n->sem_decl->a ? n->sem_decl->a->children : 0; + FeNode *ma; + fputs(cname(n->sem_decl,"fe_method"),e->out); fputc('(',e->out); + if (mp && mp->sem_type && mp->sem_type->kind==FE_TYPE_REF) { + fputc('&',e->out); emit_lvalue(e,n->a->a); + } else emit_expr(e,n->a->a); + for (ma=n->children;ma;ma=ma->next) { + fputs(", ",e->out); emit_expr(e,ma); + } + fputc(')',e->out); + return; + } else if (n->a) emit_expr(e,n->a); + else fputs(n->text ? n->text : "fe_builtin",e->out); + if (!special) { + if (n->sem_decl && n->sem_decl->kind==FE_N_FN && n->sem_decl->a) + call_param=n->sem_decl->a->children; + fputc('(',e->out); + for (x=n->children;x;x=x->next) { + FeType *want=call_param && call_param->a ? + fe_type_from_ast(&e->check->types,call_param->a) : 0; + if (x!=n->children) fputs(", ",e->out); + if (want && want->kind==FE_TYPE_SLICE && !want->ref_mut && + x->sem_type && x->sem_type->kind==FE_TYPE_SLICE && + x->sem_type->ref_mut) { + fputs(want->maker,e->out); fputc('(',e->out); + emit_expr(e,x); fputs(".p, ",e->out); + emit_expr(e,x); fputs(".n)",e->out); + } else emit_expr(e,x); + if (call_param) call_param=call_param->next; + } + fputc(')',e->out); + } +} + +/* `dst = ;` as a statement, avoiding a comma expression on the right. + + A consumed identifier lowers to `(fe_live_x=0, x)`. When dst is a struct, + Watcom crashes on a struct assignment whose right side is a comma expression + -- hard enough to take DOSBox-X down with it -- so clear the move flag as its + own statement and assign the plain name. */ +static void m7_emit_assign_stmt(FeEmitter *e, const char *dst, FeNode *src) +{ + if (src && src->kind==FE_N_IDENT && (src->flags & FE_OWN_NODE_CONSUMED) && + src->sem_type && type_needs_drop(src->sem_type)) { + pad(e); fputs("fe_live_",e->out); fputs(cname(src,"owned"),e->out); + fputs("=0;\n",e->out); + pad(e); fputs(dst,e->out); fputs(" = ",e->out); + fputs(cname(src,"fe_missing"),e->out); fputs(";\n",e->out); + return; + } + pad(e); fputs(dst,e->out); fputs(" = ",e->out); + emit_expr(e,src); fputs(";\n",e->out); +} + +static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) +{ + FeNode *x; + FeType *bt; + FeVariantType *v; + const char *op; + FeM7LazyKind lazy; + if (!n) { fputs("0",e->out); return; } + /* No feature scan: the switch below handles the node kinds this emitter + changes and its default hands everything else to emit_expr_core, so the + same path serves a unit whether or not it mentions optionals. */ + switch (n->kind) { + case FE_N_IDENT: + if ((n->flags & FE_OWN_NODE_CONSUMED) && n->sem_type && + type_needs_drop(n->sem_type)) { + fputs("(fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("=0, ",e->out); fputs(cname(n,"fe_missing"),e->out); + fputc(')',e->out); + } else fputs(cname(n,"fe_missing"),e->out); + break; + case FE_N_LITERAL: + if (fe_m7_is_null(n)) fputs("0",e->out); + else emit_expr_core(e,n); + break; + case FE_N_UNARY: + op=n->text ? n->text : ""; + if (strcmp(op,"try")==0) { + if (n->a && n->a->sem_type && + n->a->sem_type->kind==FE_TYPE_ERROR_UNION && + n->a->sem_type->error_value && + n->a->sem_type->error_value->kind!=FE_TYPE_VOID) { + fputs("(",e->out); fputs(n->aux_cname,e->out); + fputs(" = ",e->out); emit_expr(e,n->a); fputs(", ",e->out); + fputs(n->aux_cname,e->out); fputs(".v)",e->out); + } else emit_expr(e,n->a); + } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + fputs("(&",e->out); emit_lvalue(e,n->a); fputc(')',e->out); + } else if (strcmp(op,"not")==0) { + fputs("(!",e->out); emit_expr(e,n->a); fputc(')',e->out); + } else { + fputc('(',e->out); fputs(op,e->out); emit_expr(e,n->a); + fputc(')',e->out); + } + break; + case FE_N_BINARY: + lazy=fe_m7_lazy_kind(n); + if (lazy==FE_M7_LAZY_ORELSE) { + FeType *opt=n->a ? n->a->sem_type : 0; + fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs("), ",e->out); + m7_emit_present(e,opt,n->aux_cname); fputs(" ? ",e->out); + if (fe_m7_optional_uses_niche(opt->elem)) fputs(n->aux_cname,e->out); + else { fputs(n->aux_cname,e->out); fputs(".v",e->out); } + fputs(" : ",e->out); emit_expr(e,n->b); fputc(')',e->out); + } else if (lazy==FE_M7_LAZY_CATCH && !n->c) { + FeType *res=n->a ? n->a->sem_type : 0; + fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs("), ",e->out); + if (res && res->error_value && res->error_value->kind==FE_TYPE_VOID) { + fputs(n->aux_cname,e->out); fputs(" ? ",e->out); + emit_expr(e,n->b); fputs(" : 0)",e->out); + } else { + fputs(n->aux_cname,e->out); fputs(".e ? ",e->out); + emit_expr(e,n->b); fputs(" : ",e->out); + fputs(n->aux_cname,e->out); fputs(".v)",e->out); + } + } else if (lazy==FE_M7_LAZY_CATCH && n->c) { + fputs("0",e->out); + } else if ((n->text && (strcmp(n->text,"==")==0 || + strcmp(n->text,"!=")==0)) && + (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { + FeNode *value=fe_m7_is_null(n->a) ? n->b : n->a; + FeType *opt=value ? value->sem_type : 0; + if (opt && opt->kind==FE_TYPE_OPTIONAL && + !fe_m7_optional_uses_niche(opt->elem)) { + fputs("(!",e->out); emit_expr(e,value); fputs(".has)",e->out); + if (strcmp(n->text,"!=")==0) { + fputs(" == 0",e->out); + } + } else { + fputc('(',e->out); emit_expr(e,value); + fputs(strcmp(n->text,"==")==0 ? " == 0)" : " != 0)",e->out); + } + } else { + op=n->text ? n->text : "+"; + fputc('(',e->out); emit_expr(e,n->a); + if (strcmp(op,"and")==0) fputs(" && ",e->out); + else if (strcmp(op,"or")==0) fputs(" || ",e->out); + else fputs(op,e->out); + emit_expr(e,n->b); fputc(')',e->out); + } + break; + case FE_N_MEMBER: + bt=n->a ? n->a->sem_type : 0; + if (n->text && strcmp(n->text,".?")==0 && bt && + bt->kind==FE_TYPE_OPTIONAL) { + fputs(bt->unwrap_cname,e->out); fputc('(',e->out); + emit_expr(e,n->a); fputc(')',e->out); + } else if (bt && bt->kind==FE_TYPE_ENUM && bt->is_error) { + m7_emit_error_member(e,n); + } else if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && + n->b && n->b->text && strcmp(n->b->text,"^")==0) { + fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); + } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { + emit_expr(e,n->a); fputs("->",e->out); + fputs(n->b && n->b->text ? n->b->text : "member",e->out); + } else if (bt && bt->kind==FE_TYPE_ENUM && !bt->is_error) { + v=fe_type_variant(bt,n->b ? n->b->text : ""); + if (v) { fputs(v->maker,e->out); fputs("()",e->out); } + else fputs("0",e->out); + } else { + emit_expr(e,n->a); fputc('.',e->out); + if (n->b) fputs(n->b->text ? n->b->text : "member",e->out); + } + break; + case FE_N_CALL: + m7_emit_call(e,n); + break; + case FE_N_TYPE: + if (n->text && strcmp(n->text,"as")==0) { + fputs("((",e->out); fputs(m7_c_type(e,n->sem_type),e->out); + fputc(')',e->out); emit_expr(e,n->a); fputc(')',e->out); + } else emit_expr(e,n->a); + break; + case FE_N_INDEX: + bt=n->a ? n->a->sem_type : 0; + if (n->c || !n->b) { + emit_expr_core(e,n); + } else if (bt && bt->indexer) { + fputs(bt->indexer,e->out); fputc('(',e->out); + emit_expr(e,n->a); fputs(", ",e->out); emit_expr(e,n->b); + fputc(')',e->out); + } else fputs("0",e->out); + break; + case FE_N_STRUCT_INIT: + case FE_N_ARRAY_INIT: + emit_expr_core(e,n); + break; + default: + emit_expr_core(e,n); + break; + } + (void)x; +} + +/* Emit the initializer for a `const` declaration. + + A string literal normally lowers to a maker call, but C89 requires the + initializer of an aggregate -- at file scope and for automatics alike -- to be + a constant expression, and the build runs with -za. Emit the slice braced + instead. Returns non-zero when it handled the initializer. */ +static int m7_emit_const_init(FeEmitter *e, FeNode *n) +{ + if (n->kind!=FE_N_CONST || !n->b || n->b->kind!=FE_N_LITERAL || + !n->b->text || n->b->text[0]!='"') return 0; + fputs("{ (const unsigned char*)",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(", sizeof(",e->out); + emit_c_literal(e->out,n->b->text,1); + fputs(")-1 }",e->out); + return 1; +} + +static void emit_decl(FeEmitter *e, FeNode *n) +{ + pad(e); fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); + fputs(cname(n,"fe_local"),e->out); + if (n->kind==FE_N_CONST && n->b) { + fputs(" = ",e->out); + if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); + } + fputs(";\n",e->out); + if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && + type_needs_drop(n->sem_type)) { + pad(e); fputs("unsigned char fe_live_",e->out); + fputs(cname(n,"owned"),e->out); fputs("=0;\n",e->out); + } +} + +static void emit_owned_live(FeEmitter *e, FeNode *n, int value) +{ + if (n && n->sem_type && type_needs_drop(n->sem_type)) { + pad(e); fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fprintf(e->out,"=%d;\n",value); + } +} + +static void emit_value_drop(FeEmitter *e, FeNode *n) +{ + FeType *t; + t=n ? n->sem_type : 0; + if (!n || !t || !type_needs_drop(t) || + (n->flags & FE_OWN_NODE_CONSUMED) || + (n->flags & FE_OWN_NODE_DEFER_CAPTURE)) return; + pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs(") { ",e->out); + if (t->kind==FE_TYPE_OWNED) { + if (t->elem && t->elem->kind==FE_TYPE_SLICE) { + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); + fputs(".p); ",e->out); + } else { + if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) + fprintf(e->out,"%s(%s); ",t->elem->drop_cname,cname(n,"owned")); + fputs("free(",e->out); fputs(cname(n,"owned"),e->out); + fputs("); ",e->out); + } + } else if (t->drop_cname) { + fprintf(e->out,"%s(&%s); ",t->drop_cname,cname(n,"local")); + } + fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); + fputs("=0; }\n",e->out); +} + +static void emit_cleanup_block(FeEmitter *e, FeNode *n) +{ + FeNode *x; + unsigned count; + unsigned index; + unsigned seen; + unsigned depth; + count=0; + seen=0xffffffffU; + for (depth=0;depthblock_depth;++depth) + if (e->block_stack[depth]==n) { + seen=e->block_seen[depth]; + break; + } + for (x=n ? n->children : 0,index=0;x;x=x->next,++index) + if (indexkind==FE_N_DEFER || x->kind==FE_N_LET || + x->kind==FE_N_VAR)) ++count; + while (count) { + index=0; + for (x=n->children;x;x=x->next) + if ((x->kind==FE_N_DEFER || x->kind==FE_N_LET || + x->kind==FE_N_VAR) && index++==count-1U) { + if (x->kind==FE_N_DEFER) emit_stmt(e,x->a); + else emit_value_drop(e,x); + break; + } + --count; + } +} + +static void emit_cleanup_to(FeEmitter *e, unsigned floor) +{ + unsigned i; + for (i=e->block_depth;i>floor;--i) + emit_cleanup_block(e,e->block_stack[i-1U]); +} + +static void emit_param_cleanup(FeEmitter *e) +{ + FeNode *p; + if (!e->current_fn || !e->current_fn->a) return; + for (p=e->current_fn->a->children;p;p=p->next) emit_value_drop(e,p); +} + +static void emit_cleanup_all(FeEmitter *e) +{ + emit_cleanup_to(e,0); + emit_param_cleanup(e); +} + +static void emit_error_return(FeEmitter *e, const char *error_expr) +{ + FeType *ret=e->current_ret; + if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && + ret->error_value->kind!=FE_TYPE_VOID) { + fputs("return ",e->out); fputs(ret->none_cname,e->out); + fputc('(',e->out); fputs(error_expr,e->out); fputs(");\n",e->out); + } else { + fputs("return ",e->out); fputs(error_expr,e->out); fputs(";\n",e->out); + } +} + +static void m7_emit_try_error_check(FeEmitter *e, FeNode *n) +{ + FeType *result=n->a ? n->a->sem_type : 0; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + emit_cleanup_all(e); + pad(e); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) { + char error[192]; + sprintf(error,"%s.e",n->aux_cname); + emit_error_return(e,error); + } else emit_error_return(e,n->aux_cname); + --e->indent; pad(e); fputs("}\n",e->out); +} + +static void m7_emit_catch_block(FeEmitter *e, FeNode *n, + FeNode *target) +{ + FeType *result=n->a ? n->a->sem_type : 0; + FeNode *binding=n->b; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + if (binding && binding->cname) { + pad(e); fputs("unsigned short ",e->out); fputs(binding->cname,e->out); + fputs(" = ",e->out); fputs(n->aux_cname,e->out); + if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(";\n",e->out); + } + emit_stmt(e,n->c); + --e->indent; pad(e); fputs("}",e->out); + if (target && result && result->error_value && + result->error_value->kind!=FE_TYPE_VOID) { + fputs(" else {\n",e->out); ++e->indent; + pad(e); emit_lvalue(e,target); fputs(" = ",e->out); + fputs(n->aux_cname,e->out); fputs(".v;\n",e->out); + emit_owned_live(e,target,1); + --e->indent; pad(e); fputs("}",e->out); + } + fputc('\n',e->out); +} + +static void m7_emit_optional_match(FeEmitter *e, FeNode *n) +{ + FeType *opt=n->a ? n->a->sem_type : 0; + FeNode *arm; + FeNode *binding; + int first; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + first=1; + for (arm=n->children;arm;arm=arm->next) { + if (arm->text && strcmp(arm->text,"Some")==0) { + pad(e); if (!first) fputs("else ",e->out); + fputs("if (",e->out); m7_emit_present(e,opt,n->aux_cname); + fputs(") {\n",e->out); ++e->indent; + binding=arm->children; + if (binding && binding->cname) { + pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); + fputc(' ',e->out); fputs(binding->cname,e->out); fputs(" = ",e->out); + m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); + } + if (arm->a) emit_stmt(e,arm->a); + --e->indent; pad(e); fputs("}\n",e->out); + first=0; + } else if (arm->text && strcmp(arm->text,"None")==0) { + pad(e); if (!first) fputs("else ",e->out); + fputs("if (!",e->out); m7_emit_present(e,opt,n->aux_cname); + fputs(") {\n",e->out); ++e->indent; + if (arm->a) emit_stmt(e,arm->a); + --e->indent; pad(e); fputs("}\n",e->out); + first=0; + } else if (arm->text && strcmp(arm->text,"_")==0) { + pad(e); if (!first) fputs("else ",e->out); + fputs("{\n",e->out); ++e->indent; + if (arm->a) emit_stmt(e,arm->a); + --e->indent; pad(e); fputs("}\n",e->out); + first=0; + } + } +} + +static void m7_emit_if_let(FeEmitter *e, FeNode *n) +{ + FeType *opt=n->a ? n->a->sem_type : 0; + FeNode *binding=n->children; + int some=n->aux_text && strcmp(n->aux_text,"Some")==0; + pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); + if (!some) fputc('!',e->out); + m7_emit_present(e,opt,n->aux_cname); fputs(") {\n",e->out); + ++e->indent; + if (some && binding && binding->cname) { + pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); fputc(' ',e->out); + fputs(binding->cname,e->out); fputs(" = ",e->out); + m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); + } + if (n->b) emit_stmt(e,n->b); + --e->indent; pad(e); fputs("}",e->out); + if (n->c) { + fputs(" else ",e->out); + emit_stmt(e,n->c); + } + fputc('\n',e->out); +} + +static void emit_block(FeEmitter *e, FeNode *n) +{ + FeNode *x; + unsigned seen; + if (!n) { + pad(e); fputs("{}",e->out); return; + } + pad(e); fputs("{\n",e->out); ++e->indent; + if (e->block_depth<32U) { + e->block_stack[e->block_depth]=n; + e->block_seen[e->block_depth]=0; + ++e->block_depth; + } + for (x=n->children;x;x=x->next) + if (x->kind==FE_N_LET || x->kind==FE_N_VAR || x->kind==FE_N_CONST) + emit_decl(e,x); + if (e->current_fn && e->current_fn->c==n) { + if (e->current_fn->a) { + FeNode *p; + for (p=e->current_fn->a->children;p;p=p->next) + if (p->sem_type && type_needs_drop(p->sem_type)) { + pad(e); fputs("unsigned char fe_live_",e->out); + fputs(cname(p,"owned"),e->out); fputs("=1;\n",e->out); + } + } + m7_emit_temp_decls(e,n); + } + if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs(m7_c_type(e,e->current_ret),e->out); + fputs(" fe_return_value;\n",e->out); + } + seen=0; + for (x=n->children;x;x=x->next) { + ++seen; + if (e->block_depth) e->block_seen[e->block_depth-1U]=seen; + emit_stmt(e,x); + } + --e->indent; + emit_cleanup_block(e,n); + if (e->current_fn && e->current_fn->c==n) emit_param_cleanup(e); + if (e->block_depth) --e->block_depth; + if (e->fallthrough_block==n) { + pad(e); fputs("return 0;\n",e->out); + e->fallthrough_block=0; + } + pad(e); fputc('}',e->out); +} + +static void emit_stmt(FeEmitter *e, FeNode *n) +{ + FeType *result; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + emit_block(e,n); fputc('\n',e->out); break; + case FE_N_LET: + case FE_N_VAR: + if (n->b) { + if (fe_m7_is_try(n->b)) { + m7_emit_try_error_check(e,n->b); + pad(e); emit_lvalue(e,n); fputs(" = ",e->out); + fputs(n->b->aux_cname,e->out); + result=n->b->a ? n->b->a->sem_type : 0; + if (result && result->error_value && + result->error_value->kind!=FE_TYPE_VOID) fputs(".v",e->out); + fputs(";\n",e->out); emit_owned_live(e,n,1); + } else if (n->b->kind==FE_N_BINARY && n->b->c && + fe_m7_lazy_kind(n->b)==FE_M7_LAZY_CATCH) { + m7_emit_catch_block(e,n->b,n); + } else { + pad(e); emit_lvalue(e,n); fputs(" = ",e->out); + emit_expr(e,n->b); fputs(";\n",e->out); + emit_owned_live(e,n,1); + } + } + break; + case FE_N_ASSIGN: + if (n->a && n->a->kind==FE_N_IDENT) emit_value_drop(e,n->a); + pad(e); emit_lvalue(e,n->a); fputc(' ',e->out); + fputs(n->text ? n->text : "=",e->out); fputc(' ',e->out); + emit_expr(e,n->b); fputs(";\n",e->out); + if (n->a && n->a->kind==FE_N_IDENT) emit_owned_live(e,n->a,1); + break; + case FE_N_EXPR_STMT: + if (fe_m7_is_try(n->a)) { + m7_emit_try_error_check(e,n->a); + } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && + fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { + m7_emit_catch_block(e,n->a,0); + } else { + pad(e); emit_expr(e,n->a); fputs(";\n",e->out); + } + break; + case FE_N_DEFER: + break; + case FE_N_RETURN: + if (n->a && fe_m7_is_try(n->a)) { + FeNode *tr=n->a; + FeType *res=tr->a ? tr->a->sem_type : 0; + m7_emit_try_error_check(e,tr); + if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs("fe_return_value = ",e->out); + if (e->current_ret->kind==FE_TYPE_ERROR_UNION && + e->current_ret->error_value && + e->current_ret->error_value->kind!=FE_TYPE_VOID) { + fputs(e->current_ret->maker,e->out); fputs("(0, ",e->out); + fputs(tr->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".v",e->out); + fputc(')',e->out); + } else { + fputs(tr->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".v",e->out); + } + fputs(";\n",e->out); + } + emit_cleanup_all(e); + pad(e); fputs("return fe_return_value;\n",e->out); + } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && + fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { + /* A value catch-block is lowered as a temporary local success + assignment; the handler is required by the checker to exit. */ + FeNode *cx=n->a; + FeType *res=cx->a ? cx->a->sem_type : 0; + pad(e); fputs(cx->aux_cname,e->out); fputs(" = ",e->out); + emit_expr(e,cx->a); fputs(";\n",e->out); + pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + if (cx->b && cx->b->cname) { + pad(e); fputs("unsigned short ",e->out); fputs(cx->b->cname,e->out); + fputs(" = ",e->out); fputs(cx->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".e",e->out); + fputs(";\n",e->out); + } + emit_stmt(e,cx->c); + --e->indent; pad(e); fputs("}\n",e->out); + pad(e); fputs("fe_return_value = ",e->out); + fputs(cx->aux_cname,e->out); + if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) + fputs(".v",e->out); + fputs(";\n",e->out); + emit_cleanup_all(e); + pad(e); fputs("return fe_return_value;\n",e->out); + } else if (n->a && n->a->kind==FE_N_BINARY && !n->a->c && + fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH && + e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + /* Short catch in return position. As an expression this lowers to + `((tmp = X), tmp.e ? fallback : tmp.v)`, and when X carries a move + it becomes a struct assignment whose right side is itself a comma + expression -- which crashes wcc386 hard enough to take DOSBox-X + down with it. The same lowering as statements is also plainer. */ + FeNode *cx=n->a; + FeType *res=cx->a ? cx->a->sem_type : 0; + int has_value=res && res->error_value && + res->error_value->kind!=FE_TYPE_VOID; + m7_emit_assign_stmt(e,cx->aux_cname,cx->a); + pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); + if (has_value) fputs(".e",e->out); + fputs(") {\n",e->out); ++e->indent; + pad(e); fputs("fe_return_value = ",e->out); + emit_expr(e,cx->b); fputs(";\n",e->out); + --e->indent; pad(e); fputs("} else {\n",e->out); ++e->indent; + pad(e); fputs("fe_return_value = ",e->out); + fputs(cx->aux_cname,e->out); + if (has_value) fputs(".v",e->out); + fputs(";\n",e->out); + --e->indent; pad(e); fputs("}\n",e->out); + emit_cleanup_all(e); + pad(e); fputs("return fe_return_value;\n",e->out); + } else { + if (n->a && e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { + pad(e); fputs("fe_return_value = ",e->out); + emit_expr(e,n->a); fputs(";\n",e->out); + } + emit_cleanup_all(e); + pad(e); fputs("return",e->out); + if (n->a) fputs(" fe_return_value",e->out); + fputs(";\n",e->out); + } + break; + case FE_N_IF: + if (n->text && strcmp(n->text,"if let")==0) { + m7_emit_if_let(e,n); + } else { + pad(e); fputs("if (",e->out); emit_expr(e,n->a); fputs(") ",e->out); + emit_block(e,n->b); + if (n->c) { + fputs(" else ",e->out); + if (n->c->kind==FE_N_IF) emit_stmt(e,n->c); + else emit_block(e,n->c); + } + fputc('\n',e->out); + } + break; + case FE_N_MATCH: + if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OPTIONAL) + m7_emit_optional_match(e,n); + else emit_match_core(e,n,0); + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (e->loop_depth) { + emit_cleanup_to(e,e->loop_floor[e->loop_depth-1U]); + pad(e); fputs(n->kind==FE_N_BREAK ? "break;\n" : "continue;\n",e->out); + } + break; + case FE_N_WHILE: + pad(e); fputs("while (",e->out); emit_expr(e,n->a); fputs(") ",e->out); + if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; + emit_block(e,n->b); + if (e->loop_depth) --e->loop_depth; + fputc('\n',e->out); + break; + case FE_N_FOR: + emit_stmt_core(e,n); + break; + default: + emit_stmt_core(e,n); + break; + } +} + +static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) +{ + FeNode *p; + FeType *old_ret; + FeNode *old_fn; + fputs(m7_c_type(e,fn->sem_type ? fn->sem_type : + (fn->b ? fe_type_from_ast(&e->check->types,fn->b) : + fe_type_intern(&e->check->types,"void"))),e->out); + fputc(' ',e->out); fputs(cname(fn,"fe_fn"),e->out); fputc('(',e->out); + p=fn->a ? fn->a->children : 0; + if (!p) fputs("void",e->out); + while (p) { + if (p!=fn->a->children) fputs(", ",e->out); + fputs(m7_c_type(e,p->sem_type ? p->sem_type : + fe_type_from_ast(&e->check->types,p->a)),e->out); + fputc(' ',e->out); fputs(cname(p,"fe_arg"),e->out); + p=p->next; + } + fputc(')',e->out); + if (prototype) { fputs(";\n",e->out); return; } + old_ret=e->current_ret; + old_fn=e->current_fn; + e->current_ret=fn->sem_type; + e->current_fn=fn; + m7_prepare_temps(e,fn->c); + fputc(' ',e->out); + if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && + fn->sem_type->error_value && fn->sem_type->error_value->kind==FE_TYPE_VOID) + e->fallthrough_block=fn->c; + emit_block(e,fn->c); + e->current_ret=old_ret; + e->current_fn=old_fn; + fputc('\n',e->out); +} + +static void emit_main_wrapper(FeEmitter *e, FeNode *fn) +{ + FeType *ret=fn->sem_type; + if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && + ret->error_value->kind!=FE_TYPE_VOID) { + fputs("int main(void) { ",e->out); fputs(m7_c_type(e,ret),e->out); + fputs(" r = ",e->out); fputs(cname(fn,"fe_main"),e->out); + fputs("(); return r.e ? 1 : 0; }\n",e->out); + } else emit_main_wrapper_core(e,fn); +} + void fe_emit_c_program(FeEmitter *e) { FeNode *n; - FeNode *main_fn = 0; + FeNode *main_fn; FeType *type; int need_m4; + main_fn=0; need_m4=node_uses_m4(e->check->ast->root); - for (type=e->check->types.types; type; type=type->next) + for (type=e->check->types.types;type;type=type->next) if (strcmp(type->name,"io.Writer")==0) need_m4=1; - /* stdio is only reached by the M4 writer runtime (fwrite/stdout/stderr). - Parsing it costs far more than the generated body -- a 26-line unit pulls - in about 1900 lines of headers -- so leave it out when nothing uses it. */ - fputs("/* generated by fec M4 */\n#include \n#include \n#include \n", e->out); - if (need_m4) fputs("#include \n", e->out); - fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n", e->out); + /* See emit_c.c: stdio only comes in with the M4 writer runtime. */ + fputs("/* generated by fec M7 */\n#include \n#include \n#include \n",e->out); + if (need_m4) fputs("#include \n",e->out); + fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); if (e->pointer_bits==16) fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); else fputs("typedef char fe_assert_usize[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); - fputs("static void fe_trap_bounds(void) { abort(); }\nstatic unsigned short fe_error_temp;\n\n", e->out); + fputs("static void fe_trap_bounds(void) { abort(); }\nstatic unsigned short fe_error_temp;\n\n",e->out); emit_type_defs(e); if (need_m4) emit_m4_runtime(e); - emit_type_helpers(e); - for (n = e->check->ast->root ? e->check->ast->root->children : 0; - n; n = n->next) { - if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) { - fputs(ctype(e, n), e->out); - fputc(' ', e->out); - fputs(cname(n, "fe_global"), e->out); + m7_emit_type_helpers(e); + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) { + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); + fputs(cname(n,"fe_global"),e->out); if (n->b) { - fputs(" = ", e->out); - if (n->kind==FE_N_CONST && n->b->kind==FE_N_LITERAL && - n->b->text && n->b->text[0]=='"') { - fputs("{ (const unsigned char*)",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(", sizeof(",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(")-1 }",e->out); - } else emit_expr(e, n->b); + fputs(" = ",e->out); + if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); } - fputs(";\n", e->out); + fputs(";\n",e->out); } } - for (n = e->check->ast->root ? e->check->ast->root->children : 0; - n; n = n->next) - if (n->kind == FE_N_FN) { - emit_fn(e, n, 1); - if (n->text && strcmp(n->text, "main") == 0) main_fn = n; + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) { + emit_fn(e,n,1); + if (n->text && strcmp(n->text,"main")==0) main_fn=n; } - for (n = e->check->ast->root ? e->check->ast->root->children : 0; - n; n = n->next) if(n->kind==FE_N_STRUCT) { - FeNode *m; - for(m=n->children; m; m=m->next) - if(m->kind==FE_N_FN) - emit_fn(e,m,1); - } - fputc('\n', e->out); - for (n = e->check->ast->root ? e->check->ast->root->children : 0; - n; n = n->next) - if (n->kind == FE_N_FN) emit_fn(e, n, 0); - for (n = e->check->ast->root ? e->check->ast->root->children : 0; - n; n = n->next) if(n->kind==FE_N_STRUCT) { - FeNode *m; - for(m=n->children; m; m=m->next) - if(m->kind==FE_N_FN) - emit_fn(e,m,0); - } - if (main_fn) { - fputc('\n', e->out); - emit_main_wrapper(e, main_fn); - } + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) { + FeNode *m; + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) emit_fn(e,m,1); + } + fputc('\n',e->out); + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) emit_fn(e,n,0); + for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT) { + FeNode *m; + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) emit_fn(e,m,0); + } + if (main_fn) { fputc('\n',e->out); emit_main_wrapper(e,main_fn); } } diff --git a/fec/src/emitcm7.c b/fec/src/emitcm7.c deleted file mode 100644 index c91b96c..0000000 --- a/fec/src/emitcm7.c +++ /dev/null @@ -1,1420 +0,0 @@ -/* M7 C backend integration. Keep the verified M1-M6 emitter available as - an exact fast path, while M7 sources reuse its mature helpers from this - translation unit and override only expression/control-flow semantics that - changed in v0.1.8. */ -#define type_needs_drop type_needs_drop_m6 -#define emit_expr emit_expr_m6 -#define emit_stmt emit_stmt_m6 -#define emit_block emit_block_m6 -#define emit_lvalue emit_lvalue_m6 -#define emit_decl emit_decl_m6 -#define emit_owned_live emit_owned_live_m6 -#define emit_value_drop emit_value_drop_m6 -#define emit_cleanup_block emit_cleanup_block_m6 -#define emit_cleanup_to emit_cleanup_to_m6 -#define emit_param_cleanup emit_param_cleanup_m6 -#define emit_cleanup_all emit_cleanup_all_m6 -#define emit_error_return emit_error_return_m6 -#define emit_match emit_match_m6 -#define emit_fn emit_fn_m6 -#define emit_main_wrapper emit_main_wrapper_m6 -#define emit_type_defs emit_type_defs_m6 -#define fe_emit_c_init fe_emit_c_init_m6 -#define fe_emit_c_program fe_emit_c_program_m6 -#include "emit_c.c" -#undef type_needs_drop -#undef emit_expr -#undef emit_stmt -#undef emit_block -#undef emit_lvalue -#undef emit_decl -#undef emit_owned_live -#undef emit_value_drop -#undef emit_cleanup_block -#undef emit_cleanup_to -#undef emit_param_cleanup -#undef emit_cleanup_all -#undef emit_error_return -#undef emit_match -#undef emit_fn -#undef emit_main_wrapper -#undef emit_type_defs -#undef fe_emit_c_init -#undef fe_emit_c_program - -#include "m7.h" -#include "lower.h" - -static void emit_expr(FeEmitter *e, FeNode *n); -static void emit_stmt(FeEmitter *e, FeNode *n); -static void emit_block(FeEmitter *e, FeNode *n); -static void emit_lvalue(FeEmitter *e, FeNode *n); - -static int type_needs_drop(FeType *t) -{ - return fe_lower_type_needs_drop(t); -} - -static const char *m7_c_type(FeEmitter *e, FeType *t) -{ - if (!t) return "long"; - if ((t->kind==FE_TYPE_ENUM && t->is_error) || - strcmp(t->name,"core.Error")==0) - return "unsigned short"; - return fe_type_c_name(t,e->pointer_bits); -} - -static int m7_node_feature(FeNode *n) -{ - FeNode *x; - if (!n) return 0; - if (n->sem_context) return 1; - if (n->sem_type && (n->sem_type->kind==FE_TYPE_OPTIONAL || - n->sem_type->kind==FE_TYPE_ERROR_UNION || - (n->sem_type->kind==FE_TYPE_ENUM && n->sem_type->is_error))) - return 1; - if (fe_m7_is_null(n) || fe_m7_is_try(n)) return 1; - if (n->kind==FE_N_MEMBER && n->text && strcmp(n->text,".?")==0) - return 1; - if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) - return 1; - if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) - return 1; - if (n->kind==FE_N_ARM && n->text && - (strcmp(n->text,"Some")==0 || strcmp(n->text,"None")==0)) - return 1; - if (m7_node_feature(n->a) || m7_node_feature(n->b) || - m7_node_feature(n->c)) return 1; - for (x=n->children;x;x=x->next) - if (m7_node_feature(x)) return 1; - return 0; -} - -static int m7_program_feature(FeEmitter *e) -{ - FeNode *n; - FeType *t; - if (!e || !e->check) return 0; - for (t=e->check->types.types;t;t=t->next) - if (t->kind==FE_TYPE_OPTIONAL || t->kind==FE_TYPE_ERROR_UNION || - (t->kind==FE_TYPE_ENUM && t->is_error)) return 1; - for (n=e->check->ast->root ? e->check->ast->root->children : 0; - n;n=n->next) - if (m7_node_feature(n) || n->kind==FE_N_ERROR_DECL) return 1; - return 0; -} - -static char *m7_temp_name(FeEmitter *e) -{ - char number[24]; - char *p; - unsigned long len; - sprintf(number,"%u",e->temp_serial++); - len=(unsigned long)strlen("fe_m7_tmp_")+ - (unsigned long)strlen(number)+1UL; - p=(char *)fe_arena_alloc(&e->check->ast->arena,len); - if (!p) return 0; - strcpy(p,"fe_m7_tmp_"); - strcat(p,number); - return p; -} - -static int m7_needs_temp(FeNode *n) -{ - if (!n) return 0; - if (fe_m7_is_try(n)) return 1; - if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) - return 1; - if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) - return 1; - if (n->kind==FE_N_MATCH && n->a && n->a->sem_type && - n->a->sem_type->kind==FE_TYPE_OPTIONAL) - return 1; - return 0; -} - -static FeType *m7_temp_type(FeNode *n) -{ - if (!n) return 0; - if (fe_m7_is_try(n)) return n->a ? n->a->sem_type : 0; - if (n->kind==FE_N_BINARY) return n->a ? n->a->sem_type : 0; - if ((n->kind==FE_N_IF || n->kind==FE_N_MATCH) && n->a) - return n->a->sem_type; - return 0; -} - -static void m7_prepare_temps(FeEmitter *e, FeNode *n) -{ - FeNode *x; - if (!n) return; - if (m7_needs_temp(n) && !n->aux_cname) - n->aux_cname=m7_temp_name(e); - m7_prepare_temps(e,n->a); - m7_prepare_temps(e,n->b); - m7_prepare_temps(e,n->c); - for (x=n->children;x;x=x->next) m7_prepare_temps(e,x); -} - -static void m7_emit_temp_decls(FeEmitter *e, FeNode *n) -{ - FeNode *x; - FeType *t; - if (!n) return; - if (m7_needs_temp(n) && n->aux_cname) { - t=m7_temp_type(n); - if (t) { - pad(e); fputs(m7_c_type(e,t),e->out); fputc(' ',e->out); - fputs(n->aux_cname,e->out); fputs(";\n",e->out); - } - } - m7_emit_temp_decls(e,n->a); - m7_emit_temp_decls(e,n->b); - m7_emit_temp_decls(e,n->c); - for (x=n->children;x;x=x->next) m7_emit_temp_decls(e,x); -} - -static void m7_emit_type(FeEmitter *e, FeType *t) -{ - unsigned i; - unsigned j; - if (!t || t->emit_state) return; - if (t->kind==FE_TYPE_OPTIONAL) { - t->emit_state=1; - m7_emit_type(e,t->elem); - if (!fe_m7_optional_uses_niche(t->elem) && t->cname) { - fputs(t->cname,e->out); fputs(" { unsigned char has; ",e->out); - fputs(m7_c_type(e,t->elem),e->out); - fputs(" v; };\n",e->out); - } - t->emit_state=2; - return; - } - if (t->kind==FE_TYPE_ARRAY) m7_emit_type(e,t->elem); - if (t->kind==FE_TYPE_SLICE) m7_emit_type(e,t->elem); - if (t->kind==FE_TYPE_OWNED) m7_emit_type(e,t->elem); - if (t->kind==FE_TYPE_STRUCT) - for (i=0;ifield_count;++i) m7_emit_type(e,t->fields[i].type); - if (t->kind==FE_TYPE_ENUM) - for (i=0;ivariant_count;++i) - for (j=0;jvariants[i].field_count;++j) - m7_emit_type(e,t->variants[i].fields[j].type); - if (t->kind==FE_TYPE_ERROR_UNION) { - m7_emit_type(e,t->elem); - m7_emit_type(e,t->error_value); - } - emit_one_type(e,t); -} - -static void emit_type_defs(FeEmitter *e) -{ - FeType *t; - for (t=e->check->types.types;t;t=t->next) - if (t->kind==FE_TYPE_ARRAY) - fe_type_slice(&e->check->types,t->elem); - for (t=e->check->types.types;t;t=t->next) m7_emit_type(e,t); -} - -static void m7_emit_drop_access(FeEmitter *e, FeType *t, - const char *access) -{ - if (!t || !access || !type_needs_drop(t)) return; - if (t->kind==FE_TYPE_OWNED) { - if (t->elem && t->elem->kind==FE_TYPE_SLICE) { - fprintf(e->out,"if ((%s).p) { free((%s).p); (%s).p=0; } ", - access,access,access); - } else { - fprintf(e->out,"if (%s) { ",access); - if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) - fprintf(e->out,"%s(%s); ",t->elem->drop_cname,access); - fprintf(e->out,"free(%s); %s=0; } ",access,access); - } - return; - } - if (t->drop_cname) - fprintf(e->out,"%s(&(%s)); ",t->drop_cname,access); -} - -static void m7_emit_drop_helpers(FeEmitter *e) -{ - FeType *t; - FeNode *method; - unsigned i; - unsigned j; - char access[256]; - for (t=e->check->types.types;t;t=t->next) - if (type_needs_drop(t) && t->drop_cname && - (t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY || - t->kind==FE_TYPE_OPTIONAL || t->kind==FE_TYPE_ERROR_UNION)) - fprintf(e->out,"static void %s(%s *self);\n", - t->drop_cname,m7_c_type(e,t)); - for (t=e->check->types.types;t;t=t->next) { - if (!type_needs_drop(t) || !t->drop_cname) continue; - if (t->kind==FE_TYPE_STRUCT) { - fprintf(e->out,"static void %s(%s *self) { ", - t->drop_cname,m7_c_type(e,t)); - method=find_drop_method(e,t->name); - if (method) fprintf(e->out,"%s(self); ",cname(method,"fe_drop_method")); - for (i=t->field_count;i>0;--i) { - sprintf(access,"self->%s",t->fields[i-1U].name); - m7_emit_drop_access(e,t->fields[i-1U].type,access); - } - fputs("}\n",e->out); - } else if (t->kind==FE_TYPE_ARRAY) { - fprintf(e->out,"static void %s(%s *self) { unsigned long i; for (i=0; i<%lu; ++i) { ", - t->drop_cname,m7_c_type(e,t),t->length); - strcpy(access,"self->a[i]"); - m7_emit_drop_access(e,t->elem,access); - fputs("} }\n",e->out); - } else if (t->kind==FE_TYPE_OPTIONAL) { - fprintf(e->out,"static void %s(%s *self) { ", - t->drop_cname,m7_c_type(e,t)); - if (fe_m7_optional_uses_niche(t->elem)) { - fputs("if (*self) { ",e->out); - m7_emit_drop_access(e,t->elem,"*self"); - fputs("} ",e->out); - } else { - fputs("if (self->has) { ",e->out); - m7_emit_drop_access(e,t->elem,"self->v"); - fputs("self->has=0; } ",e->out); - } - fputs("}\n",e->out); - } else if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && - t->error_value->kind!=FE_TYPE_VOID) { - fprintf(e->out,"static void %s(%s *self) { if (!self->e) { ", - t->drop_cname,m7_c_type(e,t)); - m7_emit_drop_access(e,t->error_value,"self->v"); - fputs("self->e=1; } }\n",e->out); - } - } - /* Error enums are scalar codes, so their enum payload helper functions - from M3 are deliberately not emitted in the M7 path. */ - (void)j; -} - -static void m7_emit_type_helpers(FeEmitter *e) -{ - FeType *t; - FeType *st; - unsigned i; - unsigned j; - const char *ct; - FeVariantType *v; - for (t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_OPTIONAL) { - if (!fe_m7_optional_uses_niche(t->elem)) { - fprintf(e->out,"static %s %s(%s v) { %s r; r.has=1; r.v=v; return r; }\n", - m7_c_type(e,t),t->maker,m7_c_type(e,t->elem),m7_c_type(e,t)); - fprintf(e->out,"static %s %s(void) { %s r; memset(&r,0,sizeof(r)); return r; }\n", - m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); - fprintf(e->out,"static %s %s(%s x) { ", - m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); - if (!e->no_checks) fputs("if (!x.has) fe_trap_bounds(); ",e->out); - fputs("return x.v; }\n",e->out); - } else { - fprintf(e->out,"static %s %s(%s x) { ", - m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); - if (!e->no_checks) fputs("if (!x) fe_trap_bounds(); ",e->out); - fputs("return x; }\n",e->out); - } - } - if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && - t->error_value->kind!=FE_TYPE_VOID) { - fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", - m7_c_type(e,t),t->maker,m7_c_type(e,t->error_value),m7_c_type(e,t)); - if (t->none_cname) - fprintf(e->out,"static %s %s(unsigned short e) { %s r; memset(&r,0,sizeof(r)); r.e=e; return r; }\n", - m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); - if (t->error_value->kind==FE_TYPE_OWNED && - t->error_value->elem && t->error_value->elem->kind==FE_TYPE_SLICE) { - FeType *item=t->error_value->elem->elem; - fprintf(e->out,"static %s %s(unsigned long n) { %s r; r.v.p=(%s*)malloc(sizeof(%s)*n); r.v.n=n; r.e=(r.v.p || !n) ? 0 : 1; return r; }\n", - m7_c_type(e,t),t->alloc_cname,m7_c_type(e,t), - m7_c_type(e,item),m7_c_type(e,item)); - } else if (t->error_value->kind==FE_TYPE_OWNED) { - fprintf(e->out,"static %s %s(%s v) { %s r; r.v=(%s)malloc(sizeof(%s)); if(r.v) *r.v=v; r.e=r.v ? 0 : 1; return r; }\n", - m7_c_type(e,t),t->alloc_cname, - m7_c_type(e,t->error_value->elem),m7_c_type(e,t), - m7_c_type(e,t->error_value), - m7_c_type(e,t->error_value->elem)); - } - } - } - for (t=e->check->types.types;t;t=t->next) { - if (t->replace_cname) { - ct=m7_c_type(e,t); - fprintf(e->out,"static %s %s(%s *dst, %s value) { %s old=*dst; *dst=value; return old; }\n", - ct,t->replace_cname,ct,ct,ct); - } - if (t->kind==FE_TYPE_STRUCT && t->maker) { - fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); - for (i=0;ifield_count;i++) { - if (i) fputs(", ",e->out); - fputs(m7_c_type(e,t->fields[i].type),e->out); - fprintf(e->out," p%u",i); - } - fprintf(e->out,") { %s v;",m7_c_type(e,t)); - for (i=0;ifield_count;i++) - fprintf(e->out," v.%s=p%u;",t->fields[i].name,i); - fputs(" return v; }\n",e->out); - } else if (t->kind==FE_TYPE_ARRAY && t->maker) { - fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); - for (i=0;ilength;i++) { - if (i) fputs(", ",e->out); - fputs(m7_c_type(e,t->elem),e->out); - fprintf(e->out," p%u",i); - } - fprintf(e->out,") { %s v;",m7_c_type(e,t)); - for (i=0;ilength;i++) fprintf(e->out," v.a[%u]=p%u;",i,i); - fputs(" return v; }\n",e->out); - } else if (t->kind==FE_TYPE_ENUM && !t->is_error) { - for (i=0;ivariant_count;i++) { - v=&t->variants[i]; - fprintf(e->out,"static %s %s(",m7_c_type(e,t),v->maker); - for (j=0;jfield_count;j++) { - if (j) fputs(", ",e->out); - fputs(m7_c_type(e,v->fields[j].type),e->out); - fprintf(e->out," p%u",j); - } - fprintf(e->out,") { %s x; x.tag=%u;",m7_c_type(e,t),v->tag); - for (j=0;jfield_count;j++) { - if (v->field_count==1) - fprintf(e->out," x.payload.%s=p%u;",v->name,j); - else - fprintf(e->out," x.payload.%s.%s=p%u;",v->name, - v->fields[j].name,j); - } - fputs(" return x; }\n",e->out); - } - } - } - m7_emit_drop_helpers(e); - /* Reuse the mature M3 index/slice helper generator. It does not depend - on M7 drop policy and all wrapper dependencies are already emitted. */ - for (t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_ARRAY && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); - if (!e->no_checks) - fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); - fputs("return x.a[i]; }\n",e->out); - if (t->slicer) { - st=fe_type_slice(&e->check->types,t->elem); - fprintf(e->out,"static %s %s(%s *x, unsigned long a, unsigned long b) { ", - m7_c_type(e,st),t->slicer,m7_c_type(e,t)); - if (!e->no_checks) - fprintf(e->out,"if (a > b || b > %lu) fe_trap_bounds(); ",t->length); - fprintf(e->out,"return %s(x->a+a,b-a); }\n",st->maker); - fprintf(e->out,"static %s %s(%s *x) { return %s(x,0,%lu); }\n", - m7_c_type(e,st),t->full_slicer,m7_c_type(e,t),t->slicer,t->length); - fprintf(e->out,"static %s %s(%s *x, unsigned long a) { return %s(x,a,%lu); }\n", - m7_c_type(e,st),t->tail_slicer,m7_c_type(e,t),t->slicer,t->length); - } - } else if (t->kind==FE_TYPE_SLICE && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); - if (!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); - fputs("return x.p[i]; }\n",e->out); - if (t->slicer) { - fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", - m7_c_type(e,t),t->slicer,m7_c_type(e,t)); - if (!e->no_checks) - fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); - fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker); - fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n", - m7_c_type(e,t),t->full_slicer,m7_c_type(e,t),t->slicer); - fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n", - m7_c_type(e,t),t->tail_slicer,m7_c_type(e,t),t->slicer); - } - } - } -} - -static void m7_emit_present(FeEmitter *e, FeType *opt, const char *name) -{ - if (fe_m7_optional_uses_niche(opt->elem)) { - fputs("(",e->out); fputs(name,e->out); fputs(" != 0)",e->out); - } else { - fputs(name,e->out); fputs(".has",e->out); - } -} - -static void m7_emit_payload_var(FeEmitter *e, FeType *opt, const char *name) -{ - (void)e; - fputs(name,e->out); - if (!fe_m7_optional_uses_niche(opt->elem)) fputs(".v",e->out); -} - -static void m7_emit_error_member(FeEmitter *e, FeNode *n) -{ - FeVariantType *v; - FeType *t; - t=n && n->a ? n->a->sem_type : 0; - v=t && t->kind==FE_TYPE_ENUM ? - fe_type_variant(t,n->b ? n->b->text : "") : 0; - if (v) fprintf(e->out,"%u",v->tag); - else fputs("0",e->out); -} - -static void m7_emit_raw_expr(FeEmitter *e, FeNode *n); - -static void m7_emit_contextual(FeEmitter *e, FeNode *n) -{ - FeType *ctx; - FeType *actual; - FeType *error_type; - ctx=n ? n->sem_context : 0; - actual=n ? n->sem_type : 0; - if (!ctx) { m7_emit_raw_expr(e,n); return; } - if (ctx->kind==FE_TYPE_OPTIONAL) { - if (fe_m7_is_null(n)) { - if (fe_m7_optional_uses_niche(ctx->elem)) fputs("0",e->out); - else { fputs(ctx->none_cname,e->out); fputs("()",e->out); } - return; - } - if (fe_m7_optional_uses_niche(ctx->elem)) { - m7_emit_raw_expr(e,n); - } else { - fputs(ctx->maker,e->out); fputc('(',e->out); - m7_emit_raw_expr(e,n); fputc(')',e->out); - } - return; - } - if (ctx->kind==FE_TYPE_ERROR_UNION) { - error_type=ctx->elem; - if (!error_type) error_type=fe_type_intern(&e->check->types,"core.Error"); - if (actual && fe_type_equal(actual,ctx->error_value)) { - if (ctx->error_value->kind==FE_TYPE_VOID) fputs("0",e->out); - else { - fputs(ctx->maker,e->out); fputs("(0, ",e->out); - m7_emit_raw_expr(e,n); fputc(')',e->out); - } - return; - } - if (actual && fe_type_equal(actual,error_type)) { - if (ctx->error_value->kind==FE_TYPE_VOID) - m7_emit_raw_expr(e,n); - else { - fputs(ctx->none_cname,e->out); fputc('(',e->out); - m7_emit_raw_expr(e,n); fputc(')',e->out); - } - return; - } - } - m7_emit_raw_expr(e,n); -} - -static void emit_expr(FeEmitter *e, FeNode *n) -{ - if (!n) { fputs("0",e->out); return; } - if (n->sem_context) m7_emit_contextual(e,n); - else m7_emit_raw_expr(e,n); -} - -static void emit_lvalue(FeEmitter *e, FeNode *n) -{ - FeType *bt; - if (!n) { fputs("fe_bad_lvalue",e->out); return; } - if (n->kind==FE_N_IDENT) { - fputs(cname(n,"fe_local"),e->out); - return; - } - /* A declaration names its own storage. The initializer for `let`/`var` is - emitted as a separate assignment statement, so the declaration node is - handed here as the target; without this it falls through to the raw - expression path, which emits a declaration as "0" and produces `0 = ...`. */ - if (n->kind==FE_N_LET || n->kind==FE_N_VAR || n->kind==FE_N_CONST) { - fputs(cname(n,"fe_local"),e->out); - return; - } - if (n->kind==FE_N_MEMBER) { - bt=n->a ? n->a->sem_type : 0; - if (n->text && strcmp(n->text,".?")==0) { - emit_expr(e,n); - return; - } - if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); - } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { - emit_expr(e,n->a); fputs("->",e->out); - fputs(n->b && n->b->text ? n->b->text : "member",e->out); - } else { - emit_lvalue(e,n->a); fputc('.',e->out); - fputs(n->b && n->b->text ? n->b->text : "member",e->out); - } - return; - } - if (n->kind==FE_N_INDEX) { - bt=n->a ? n->a->sem_type : 0; - emit_lvalue(e,n->a); - fputs(bt && bt->kind==FE_TYPE_ARRAY ? ".a[" : ".p[",e->out); - emit_expr(e,n->b); fputc(']',e->out); - return; - } - m7_emit_raw_expr(e,n); -} - -static void m7_emit_call(FeEmitter *e, FeNode *n) -{ - FeNode *x; - FeNode *call_param; - FeVariantType *v; - int special; - call_param=0; - special=0; - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"destroy")==0 && n->children) { - FeNode *arg=n->children; - fputs("(free(",e->out); emit_expr(e,arg); - if (arg->sem_type && arg->sem_type->kind==FE_TYPE_OWNED && - arg->sem_type->elem && arg->sem_type->elem->kind==FE_TYPE_SLICE) - fputs(".p",e->out); - fputc(')',e->out); - if (arg->kind==FE_N_IDENT) { - fputs(", ",e->out); emit_lvalue(e,arg); - if (arg->sem_type && arg->sem_type->elem && - arg->sem_type->elem->kind==FE_TYPE_SLICE) fputs(".p=0",e->out); - else fputs("=0",e->out); - fputs(", fe_live_",e->out); fputs(cname(arg,"owned"),e->out); - fputs("=0",e->out); - } - fputs(", 0)",e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"replace")==0 && n->children && - n->children->next && n->sem_type) { - fputs(n->sem_type->replace_cname ? n->sem_type->replace_cname : - "fe_bad_replace",e->out); - fputc('(',e->out); emit_expr(e,n->children); fputs(", ",e->out); - emit_expr(e,n->children->next); fputc(')',e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"create")==0 && n->children) { - FeType *created=n->children->sem_type; - FeType *owned=fe_type_owned(&e->check->types,created); - FeType *result=fe_type_error_union(&e->check->types,owned); - fputs(result->alloc_cname ? result->alloc_cname : "fe_bad_alloc",e->out); - fputc('(',e->out); emit_expr(e,n->children); fputc(')',e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"alloc_slice")==0 && n->children && - n->children->next) { - FeType *result=n->sem_type; - fputs(result && result->alloc_cname ? result->alloc_cname : - "fe_bad_slice_alloc",e->out); - fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out); - return; - } - if (n->text && (strcmp(n->text,"@print")==0 || - strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { - emit_m4_builtin(e,n); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"null_writer")==0) { - fputs("fe_m4_null_writer()",e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM && - !n->a->a->sem_type->is_error) { - v=fe_type_variant(n->a->a->sem_type,n->a->b ? n->a->b->text : ""); - fputs(v ? v->maker : "fe_bad_variant",e->out); - } else if (n->a && n->a->kind==FE_N_MEMBER && n->sem_decl && - n->sem_decl->kind==FE_N_FN) { - FeNode *mp=n->sem_decl->a ? n->sem_decl->a->children : 0; - FeNode *ma; - fputs(cname(n->sem_decl,"fe_method"),e->out); fputc('(',e->out); - if (mp && mp->sem_type && mp->sem_type->kind==FE_TYPE_REF) { - fputc('&',e->out); emit_lvalue(e,n->a->a); - } else emit_expr(e,n->a->a); - for (ma=n->children;ma;ma=ma->next) { - fputs(", ",e->out); emit_expr(e,ma); - } - fputc(')',e->out); - return; - } else if (n->a) emit_expr(e,n->a); - else fputs(n->text ? n->text : "fe_builtin",e->out); - if (!special) { - if (n->sem_decl && n->sem_decl->kind==FE_N_FN && n->sem_decl->a) - call_param=n->sem_decl->a->children; - fputc('(',e->out); - for (x=n->children;x;x=x->next) { - FeType *want=call_param && call_param->a ? - fe_type_from_ast(&e->check->types,call_param->a) : 0; - if (x!=n->children) fputs(", ",e->out); - if (want && want->kind==FE_TYPE_SLICE && !want->ref_mut && - x->sem_type && x->sem_type->kind==FE_TYPE_SLICE && - x->sem_type->ref_mut) { - fputs(want->maker,e->out); fputc('(',e->out); - emit_expr(e,x); fputs(".p, ",e->out); - emit_expr(e,x); fputs(".n)",e->out); - } else emit_expr(e,x); - if (call_param) call_param=call_param->next; - } - fputc(')',e->out); - } -} - -/* `dst = ;` as a statement, avoiding a comma expression on the right. - - A consumed identifier lowers to `(fe_live_x=0, x)`. When dst is a struct, - Watcom crashes on a struct assignment whose right side is a comma expression - -- hard enough to take DOSBox-X down with it -- so clear the move flag as its - own statement and assign the plain name. */ -static void m7_emit_assign_stmt(FeEmitter *e, const char *dst, FeNode *src) -{ - if (src && src->kind==FE_N_IDENT && (src->flags & FE_OWN_NODE_CONSUMED) && - src->sem_type && type_needs_drop(src->sem_type)) { - pad(e); fputs("fe_live_",e->out); fputs(cname(src,"owned"),e->out); - fputs("=0;\n",e->out); - pad(e); fputs(dst,e->out); fputs(" = ",e->out); - fputs(cname(src,"fe_missing"),e->out); fputs(";\n",e->out); - return; - } - pad(e); fputs(dst,e->out); fputs(" = ",e->out); - emit_expr(e,src); fputs(";\n",e->out); -} - -static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) -{ - FeNode *x; - FeType *bt; - FeVariantType *v; - const char *op; - FeM7LazyKind lazy; - if (!n) { fputs("0",e->out); return; } - if (!m7_node_feature(n)) { - emit_expr_m6(e,n); - return; - } - switch (n->kind) { - case FE_N_IDENT: - if ((n->flags & FE_OWN_NODE_CONSUMED) && n->sem_type && - type_needs_drop(n->sem_type)) { - fputs("(fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs("=0, ",e->out); fputs(cname(n,"fe_missing"),e->out); - fputc(')',e->out); - } else fputs(cname(n,"fe_missing"),e->out); - break; - case FE_N_LITERAL: - if (fe_m7_is_null(n)) fputs("0",e->out); - else emit_expr_m6(e,n); - break; - case FE_N_UNARY: - op=n->text ? n->text : ""; - if (strcmp(op,"try")==0) { - if (n->a && n->a->sem_type && - n->a->sem_type->kind==FE_TYPE_ERROR_UNION && - n->a->sem_type->error_value && - n->a->sem_type->error_value->kind!=FE_TYPE_VOID) { - fputs("(",e->out); fputs(n->aux_cname,e->out); - fputs(" = ",e->out); emit_expr(e,n->a); fputs(", ",e->out); - fputs(n->aux_cname,e->out); fputs(".v)",e->out); - } else emit_expr(e,n->a); - } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { - fputs("(&",e->out); emit_lvalue(e,n->a); fputc(')',e->out); - } else if (strcmp(op,"not")==0) { - fputs("(!",e->out); emit_expr(e,n->a); fputc(')',e->out); - } else { - fputc('(',e->out); fputs(op,e->out); emit_expr(e,n->a); - fputc(')',e->out); - } - break; - case FE_N_BINARY: - lazy=fe_m7_lazy_kind(n); - if (lazy==FE_M7_LAZY_ORELSE) { - FeType *opt=n->a ? n->a->sem_type : 0; - fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs("), ",e->out); - m7_emit_present(e,opt,n->aux_cname); fputs(" ? ",e->out); - if (fe_m7_optional_uses_niche(opt->elem)) fputs(n->aux_cname,e->out); - else { fputs(n->aux_cname,e->out); fputs(".v",e->out); } - fputs(" : ",e->out); emit_expr(e,n->b); fputc(')',e->out); - } else if (lazy==FE_M7_LAZY_CATCH && !n->c) { - FeType *res=n->a ? n->a->sem_type : 0; - fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs("), ",e->out); - if (res && res->error_value && res->error_value->kind==FE_TYPE_VOID) { - fputs(n->aux_cname,e->out); fputs(" ? ",e->out); - emit_expr(e,n->b); fputs(" : 0)",e->out); - } else { - fputs(n->aux_cname,e->out); fputs(".e ? ",e->out); - emit_expr(e,n->b); fputs(" : ",e->out); - fputs(n->aux_cname,e->out); fputs(".v)",e->out); - } - } else if (lazy==FE_M7_LAZY_CATCH && n->c) { - fputs("0",e->out); - } else if ((n->text && (strcmp(n->text,"==")==0 || - strcmp(n->text,"!=")==0)) && - (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { - FeNode *value=fe_m7_is_null(n->a) ? n->b : n->a; - FeType *opt=value ? value->sem_type : 0; - if (opt && opt->kind==FE_TYPE_OPTIONAL && - !fe_m7_optional_uses_niche(opt->elem)) { - fputs("(!",e->out); emit_expr(e,value); fputs(".has)",e->out); - if (strcmp(n->text,"!=")==0) { - fputs(" == 0",e->out); - } - } else { - fputc('(',e->out); emit_expr(e,value); - fputs(strcmp(n->text,"==")==0 ? " == 0)" : " != 0)",e->out); - } - } else { - op=n->text ? n->text : "+"; - fputc('(',e->out); emit_expr(e,n->a); - if (strcmp(op,"and")==0) fputs(" && ",e->out); - else if (strcmp(op,"or")==0) fputs(" || ",e->out); - else fputs(op,e->out); - emit_expr(e,n->b); fputc(')',e->out); - } - break; - case FE_N_MEMBER: - bt=n->a ? n->a->sem_type : 0; - if (n->text && strcmp(n->text,".?")==0 && bt && - bt->kind==FE_TYPE_OPTIONAL) { - fputs(bt->unwrap_cname,e->out); fputc('(',e->out); - emit_expr(e,n->a); fputc(')',e->out); - } else if (bt && bt->kind==FE_TYPE_ENUM && bt->is_error) { - m7_emit_error_member(e,n); - } else if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); - } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { - emit_expr(e,n->a); fputs("->",e->out); - fputs(n->b && n->b->text ? n->b->text : "member",e->out); - } else if (bt && bt->kind==FE_TYPE_ENUM && !bt->is_error) { - v=fe_type_variant(bt,n->b ? n->b->text : ""); - if (v) { fputs(v->maker,e->out); fputs("()",e->out); } - else fputs("0",e->out); - } else { - emit_expr(e,n->a); fputc('.',e->out); - if (n->b) fputs(n->b->text ? n->b->text : "member",e->out); - } - break; - case FE_N_CALL: - m7_emit_call(e,n); - break; - case FE_N_TYPE: - if (n->text && strcmp(n->text,"as")==0) { - fputs("((",e->out); fputs(m7_c_type(e,n->sem_type),e->out); - fputc(')',e->out); emit_expr(e,n->a); fputc(')',e->out); - } else emit_expr(e,n->a); - break; - case FE_N_INDEX: - bt=n->a ? n->a->sem_type : 0; - if (n->c || !n->b) { - emit_expr_m6(e,n); - } else if (bt && bt->indexer) { - fputs(bt->indexer,e->out); fputc('(',e->out); - emit_expr(e,n->a); fputs(", ",e->out); emit_expr(e,n->b); - fputc(')',e->out); - } else fputs("0",e->out); - break; - case FE_N_STRUCT_INIT: - case FE_N_ARRAY_INIT: - emit_expr_m6(e,n); - break; - default: - emit_expr_m6(e,n); - break; - } - (void)x; -} - -/* Emit the initializer for a `const` declaration. - - A string literal normally lowers to a maker call, but C89 requires the - initializer of an aggregate -- at file scope and for automatics alike -- to be - a constant expression, and the build runs with -za. Emit the slice braced - instead. Returns non-zero when it handled the initializer. */ -static int m7_emit_const_init(FeEmitter *e, FeNode *n) -{ - if (n->kind!=FE_N_CONST || !n->b || n->b->kind!=FE_N_LITERAL || - !n->b->text || n->b->text[0]!='"') return 0; - fputs("{ (const unsigned char*)",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(", sizeof(",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(")-1 }",e->out); - return 1; -} - -static void emit_decl(FeEmitter *e, FeNode *n) -{ - pad(e); fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); - fputs(cname(n,"fe_local"),e->out); - if (n->kind==FE_N_CONST && n->b) { - fputs(" = ",e->out); - if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); - } - fputs(";\n",e->out); - if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && - type_needs_drop(n->sem_type)) { - pad(e); fputs("unsigned char fe_live_",e->out); - fputs(cname(n,"owned"),e->out); fputs("=0;\n",e->out); - } -} - -static void emit_owned_live(FeEmitter *e, FeNode *n, int value) -{ - if (n && n->sem_type && type_needs_drop(n->sem_type)) { - pad(e); fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fprintf(e->out,"=%d;\n",value); - } -} - -static void emit_value_drop(FeEmitter *e, FeNode *n) -{ - FeType *t; - t=n ? n->sem_type : 0; - if (!n || !t || !type_needs_drop(t) || - (n->flags & FE_OWN_NODE_CONSUMED) || - (n->flags & FE_OWN_NODE_DEFER_CAPTURE)) return; - pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs(") { ",e->out); - if (t->kind==FE_TYPE_OWNED) { - if (t->elem && t->elem->kind==FE_TYPE_SLICE) { - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); - fputs(".p); ",e->out); - } else { - if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) - fprintf(e->out,"%s(%s); ",t->elem->drop_cname,cname(n,"owned")); - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); - fputs("); ",e->out); - } - } else if (t->drop_cname) { - fprintf(e->out,"%s(&%s); ",t->drop_cname,cname(n,"local")); - } - fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs("=0; }\n",e->out); -} - -static void emit_cleanup_block(FeEmitter *e, FeNode *n) -{ - FeNode *x; - unsigned count; - unsigned index; - unsigned seen; - unsigned depth; - count=0; - seen=0xffffffffU; - for (depth=0;depthblock_depth;++depth) - if (e->block_stack[depth]==n) { - seen=e->block_seen[depth]; - break; - } - for (x=n ? n->children : 0,index=0;x;x=x->next,++index) - if (indexkind==FE_N_DEFER || x->kind==FE_N_LET || - x->kind==FE_N_VAR)) ++count; - while (count) { - index=0; - for (x=n->children;x;x=x->next) - if ((x->kind==FE_N_DEFER || x->kind==FE_N_LET || - x->kind==FE_N_VAR) && index++==count-1U) { - if (x->kind==FE_N_DEFER) emit_stmt(e,x->a); - else emit_value_drop(e,x); - break; - } - --count; - } -} - -static void emit_cleanup_to(FeEmitter *e, unsigned floor) -{ - unsigned i; - for (i=e->block_depth;i>floor;--i) - emit_cleanup_block(e,e->block_stack[i-1U]); -} - -static void emit_param_cleanup(FeEmitter *e) -{ - FeNode *p; - if (!e->current_fn || !e->current_fn->a) return; - for (p=e->current_fn->a->children;p;p=p->next) emit_value_drop(e,p); -} - -static void emit_cleanup_all(FeEmitter *e) -{ - emit_cleanup_to(e,0); - emit_param_cleanup(e); -} - -static void emit_error_return(FeEmitter *e, const char *error_expr) -{ - FeType *ret=e->current_ret; - if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && - ret->error_value->kind!=FE_TYPE_VOID) { - fputs("return ",e->out); fputs(ret->none_cname,e->out); - fputc('(',e->out); fputs(error_expr,e->out); fputs(");\n",e->out); - } else { - fputs("return ",e->out); fputs(error_expr,e->out); fputs(";\n",e->out); - } -} - -static void m7_emit_try_error_check(FeEmitter *e, FeNode *n) -{ - FeType *result=n->a ? n->a->sem_type : 0; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - emit_cleanup_all(e); - pad(e); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) { - char error[192]; - sprintf(error,"%s.e",n->aux_cname); - emit_error_return(e,error); - } else emit_error_return(e,n->aux_cname); - --e->indent; pad(e); fputs("}\n",e->out); -} - -static void m7_emit_catch_block(FeEmitter *e, FeNode *n, - FeNode *target) -{ - FeType *result=n->a ? n->a->sem_type : 0; - FeNode *binding=n->b; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - if (binding && binding->cname) { - pad(e); fputs("unsigned short ",e->out); fputs(binding->cname,e->out); - fputs(" = ",e->out); fputs(n->aux_cname,e->out); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(";\n",e->out); - } - emit_stmt(e,n->c); - --e->indent; pad(e); fputs("}",e->out); - if (target && result && result->error_value && - result->error_value->kind!=FE_TYPE_VOID) { - fputs(" else {\n",e->out); ++e->indent; - pad(e); emit_lvalue(e,target); fputs(" = ",e->out); - fputs(n->aux_cname,e->out); fputs(".v;\n",e->out); - emit_owned_live(e,target,1); - --e->indent; pad(e); fputs("}",e->out); - } - fputc('\n',e->out); -} - -static void m7_emit_optional_match(FeEmitter *e, FeNode *n) -{ - FeType *opt=n->a ? n->a->sem_type : 0; - FeNode *arm; - FeNode *binding; - int first; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - first=1; - for (arm=n->children;arm;arm=arm->next) { - if (arm->text && strcmp(arm->text,"Some")==0) { - pad(e); if (!first) fputs("else ",e->out); - fputs("if (",e->out); m7_emit_present(e,opt,n->aux_cname); - fputs(") {\n",e->out); ++e->indent; - binding=arm->children; - if (binding && binding->cname) { - pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); - fputc(' ',e->out); fputs(binding->cname,e->out); fputs(" = ",e->out); - m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); - } - if (arm->a) emit_stmt(e,arm->a); - --e->indent; pad(e); fputs("}\n",e->out); - first=0; - } else if (arm->text && strcmp(arm->text,"None")==0) { - pad(e); if (!first) fputs("else ",e->out); - fputs("if (!",e->out); m7_emit_present(e,opt,n->aux_cname); - fputs(") {\n",e->out); ++e->indent; - if (arm->a) emit_stmt(e,arm->a); - --e->indent; pad(e); fputs("}\n",e->out); - first=0; - } else if (arm->text && strcmp(arm->text,"_")==0) { - pad(e); if (!first) fputs("else ",e->out); - fputs("{\n",e->out); ++e->indent; - if (arm->a) emit_stmt(e,arm->a); - --e->indent; pad(e); fputs("}\n",e->out); - first=0; - } - } -} - -static void m7_emit_if_let(FeEmitter *e, FeNode *n) -{ - FeType *opt=n->a ? n->a->sem_type : 0; - FeNode *binding=n->children; - int some=n->aux_text && strcmp(n->aux_text,"Some")==0; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); - if (!some) fputc('!',e->out); - m7_emit_present(e,opt,n->aux_cname); fputs(") {\n",e->out); - ++e->indent; - if (some && binding && binding->cname) { - pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); fputc(' ',e->out); - fputs(binding->cname,e->out); fputs(" = ",e->out); - m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); - } - if (n->b) emit_stmt(e,n->b); - --e->indent; pad(e); fputs("}",e->out); - if (n->c) { - fputs(" else ",e->out); - emit_stmt(e,n->c); - } - fputc('\n',e->out); -} - -static void emit_block(FeEmitter *e, FeNode *n) -{ - FeNode *x; - unsigned seen; - if (!n) { - pad(e); fputs("{}",e->out); return; - } - pad(e); fputs("{\n",e->out); ++e->indent; - if (e->block_depth<32U) { - e->block_stack[e->block_depth]=n; - e->block_seen[e->block_depth]=0; - ++e->block_depth; - } - for (x=n->children;x;x=x->next) - if (x->kind==FE_N_LET || x->kind==FE_N_VAR || x->kind==FE_N_CONST) - emit_decl(e,x); - if (e->current_fn && e->current_fn->c==n) { - if (e->current_fn->a) { - FeNode *p; - for (p=e->current_fn->a->children;p;p=p->next) - if (p->sem_type && type_needs_drop(p->sem_type)) { - pad(e); fputs("unsigned char fe_live_",e->out); - fputs(cname(p,"owned"),e->out); fputs("=1;\n",e->out); - } - } - m7_emit_temp_decls(e,n); - } - if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs(m7_c_type(e,e->current_ret),e->out); - fputs(" fe_return_value;\n",e->out); - } - seen=0; - for (x=n->children;x;x=x->next) { - ++seen; - if (e->block_depth) e->block_seen[e->block_depth-1U]=seen; - emit_stmt(e,x); - } - --e->indent; - emit_cleanup_block(e,n); - if (e->current_fn && e->current_fn->c==n) emit_param_cleanup(e); - if (e->block_depth) --e->block_depth; - if (e->fallthrough_block==n) { - pad(e); fputs("return 0;\n",e->out); - e->fallthrough_block=0; - } - pad(e); fputc('}',e->out); -} - -static void emit_stmt(FeEmitter *e, FeNode *n) -{ - FeType *result; - if (!n) return; - switch (n->kind) { - case FE_N_BLOCK: - emit_block(e,n); fputc('\n',e->out); break; - case FE_N_LET: - case FE_N_VAR: - if (n->b) { - if (fe_m7_is_try(n->b)) { - m7_emit_try_error_check(e,n->b); - pad(e); emit_lvalue(e,n); fputs(" = ",e->out); - fputs(n->b->aux_cname,e->out); - result=n->b->a ? n->b->a->sem_type : 0; - if (result && result->error_value && - result->error_value->kind!=FE_TYPE_VOID) fputs(".v",e->out); - fputs(";\n",e->out); emit_owned_live(e,n,1); - } else if (n->b->kind==FE_N_BINARY && n->b->c && - fe_m7_lazy_kind(n->b)==FE_M7_LAZY_CATCH) { - m7_emit_catch_block(e,n->b,n); - } else { - pad(e); emit_lvalue(e,n); fputs(" = ",e->out); - emit_expr(e,n->b); fputs(";\n",e->out); - emit_owned_live(e,n,1); - } - } - break; - case FE_N_ASSIGN: - if (n->a && n->a->kind==FE_N_IDENT) emit_value_drop(e,n->a); - pad(e); emit_lvalue(e,n->a); fputc(' ',e->out); - fputs(n->text ? n->text : "=",e->out); fputc(' ',e->out); - emit_expr(e,n->b); fputs(";\n",e->out); - if (n->a && n->a->kind==FE_N_IDENT) emit_owned_live(e,n->a,1); - break; - case FE_N_EXPR_STMT: - if (fe_m7_is_try(n->a)) { - m7_emit_try_error_check(e,n->a); - } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && - fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { - m7_emit_catch_block(e,n->a,0); - } else { - pad(e); emit_expr(e,n->a); fputs(";\n",e->out); - } - break; - case FE_N_DEFER: - break; - case FE_N_RETURN: - if (n->a && fe_m7_is_try(n->a)) { - FeNode *tr=n->a; - FeType *res=tr->a ? tr->a->sem_type : 0; - m7_emit_try_error_check(e,tr); - if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs("fe_return_value = ",e->out); - if (e->current_ret->kind==FE_TYPE_ERROR_UNION && - e->current_ret->error_value && - e->current_ret->error_value->kind!=FE_TYPE_VOID) { - fputs(e->current_ret->maker,e->out); fputs("(0, ",e->out); - fputs(tr->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".v",e->out); - fputc(')',e->out); - } else { - fputs(tr->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".v",e->out); - } - fputs(";\n",e->out); - } - emit_cleanup_all(e); - pad(e); fputs("return fe_return_value;\n",e->out); - } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && - fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { - /* A value catch-block is lowered as a temporary local success - assignment; the handler is required by the checker to exit. */ - FeNode *cx=n->a; - FeType *res=cx->a ? cx->a->sem_type : 0; - pad(e); fputs(cx->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,cx->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - if (cx->b && cx->b->cname) { - pad(e); fputs("unsigned short ",e->out); fputs(cx->b->cname,e->out); - fputs(" = ",e->out); fputs(cx->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(";\n",e->out); - } - emit_stmt(e,cx->c); - --e->indent; pad(e); fputs("}\n",e->out); - pad(e); fputs("fe_return_value = ",e->out); - fputs(cx->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".v",e->out); - fputs(";\n",e->out); - emit_cleanup_all(e); - pad(e); fputs("return fe_return_value;\n",e->out); - } else if (n->a && n->a->kind==FE_N_BINARY && !n->a->c && - fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH && - e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - /* Short catch in return position. As an expression this lowers to - `((tmp = X), tmp.e ? fallback : tmp.v)`, and when X carries a move - it becomes a struct assignment whose right side is itself a comma - expression -- which crashes wcc386 hard enough to take DOSBox-X - down with it. The same lowering as statements is also plainer. */ - FeNode *cx=n->a; - FeType *res=cx->a ? cx->a->sem_type : 0; - int has_value=res && res->error_value && - res->error_value->kind!=FE_TYPE_VOID; - m7_emit_assign_stmt(e,cx->aux_cname,cx->a); - pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); - if (has_value) fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - pad(e); fputs("fe_return_value = ",e->out); - emit_expr(e,cx->b); fputs(";\n",e->out); - --e->indent; pad(e); fputs("} else {\n",e->out); ++e->indent; - pad(e); fputs("fe_return_value = ",e->out); - fputs(cx->aux_cname,e->out); - if (has_value) fputs(".v",e->out); - fputs(";\n",e->out); - --e->indent; pad(e); fputs("}\n",e->out); - emit_cleanup_all(e); - pad(e); fputs("return fe_return_value;\n",e->out); - } else { - if (n->a && e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs("fe_return_value = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - } - emit_cleanup_all(e); - pad(e); fputs("return",e->out); - if (n->a) fputs(" fe_return_value",e->out); - fputs(";\n",e->out); - } - break; - case FE_N_IF: - if (n->text && strcmp(n->text,"if let")==0) { - m7_emit_if_let(e,n); - } else { - pad(e); fputs("if (",e->out); emit_expr(e,n->a); fputs(") ",e->out); - emit_block(e,n->b); - if (n->c) { - fputs(" else ",e->out); - if (n->c->kind==FE_N_IF) emit_stmt(e,n->c); - else emit_block(e,n->c); - } - fputc('\n',e->out); - } - break; - case FE_N_MATCH: - if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OPTIONAL) - m7_emit_optional_match(e,n); - else emit_match_m6(e,n,0); - break; - case FE_N_BREAK: - case FE_N_CONTINUE: - if (e->loop_depth) { - emit_cleanup_to(e,e->loop_floor[e->loop_depth-1U]); - pad(e); fputs(n->kind==FE_N_BREAK ? "break;\n" : "continue;\n",e->out); - } - break; - case FE_N_WHILE: - pad(e); fputs("while (",e->out); emit_expr(e,n->a); fputs(") ",e->out); - if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; - emit_block(e,n->b); - if (e->loop_depth) --e->loop_depth; - fputc('\n',e->out); - break; - case FE_N_FOR: - emit_stmt_m6(e,n); - break; - default: - emit_stmt_m6(e,n); - break; - } -} - -static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) -{ - FeNode *p; - FeType *old_ret; - FeNode *old_fn; - fputs(m7_c_type(e,fn->sem_type ? fn->sem_type : - (fn->b ? fe_type_from_ast(&e->check->types,fn->b) : - fe_type_intern(&e->check->types,"void"))),e->out); - fputc(' ',e->out); fputs(cname(fn,"fe_fn"),e->out); fputc('(',e->out); - p=fn->a ? fn->a->children : 0; - if (!p) fputs("void",e->out); - while (p) { - if (p!=fn->a->children) fputs(", ",e->out); - fputs(m7_c_type(e,p->sem_type ? p->sem_type : - fe_type_from_ast(&e->check->types,p->a)),e->out); - fputc(' ',e->out); fputs(cname(p,"fe_arg"),e->out); - p=p->next; - } - fputc(')',e->out); - if (prototype) { fputs(";\n",e->out); return; } - old_ret=e->current_ret; - old_fn=e->current_fn; - e->current_ret=fn->sem_type; - e->current_fn=fn; - m7_prepare_temps(e,fn->c); - fputc(' ',e->out); - if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && - fn->sem_type->error_value && fn->sem_type->error_value->kind==FE_TYPE_VOID) - e->fallthrough_block=fn->c; - emit_block(e,fn->c); - e->current_ret=old_ret; - e->current_fn=old_fn; - fputc('\n',e->out); -} - -static void emit_main_wrapper(FeEmitter *e, FeNode *fn) -{ - FeType *ret=fn->sem_type; - if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && - ret->error_value->kind!=FE_TYPE_VOID) { - fputs("int main(void) { ",e->out); fputs(m7_c_type(e,ret),e->out); - fputs(" r = ",e->out); fputs(cname(fn,"fe_main"),e->out); - fputs("(); return r.e ? 1 : 0; }\n",e->out); - } else emit_main_wrapper_m6(e,fn); -} - -void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, - unsigned pointer_bits, int no_checks) -{ - fe_emit_c_init_m6(e,out,check,pointer_bits,no_checks); -} - -void fe_emit_c_program(FeEmitter *e) -{ - FeNode *n; - FeNode *main_fn; - FeType *type; - int need_m4; - if (!m7_program_feature(e)) { - fe_emit_c_program_m6(e); - return; - } - main_fn=0; - need_m4=node_uses_m4(e->check->ast->root); - for (type=e->check->types.types;type;type=type->next) - if (strcmp(type->name,"io.Writer")==0) need_m4=1; - /* See emit_c.c: stdio only comes in with the M4 writer runtime. */ - fputs("/* generated by fec M7 */\n#include \n#include \n#include \n",e->out); - if (need_m4) fputs("#include \n",e->out); - fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); - if (e->pointer_bits==16) - fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); - else - fputs("typedef char fe_assert_usize[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); - fputs("static void fe_trap_bounds(void) { abort(); }\nstatic unsigned short fe_error_temp;\n\n",e->out); - emit_type_defs(e); - if (need_m4) emit_m4_runtime(e); - m7_emit_type_helpers(e); - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) { - if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { - fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); - fputs(cname(n,"fe_global"),e->out); - if (n->b) { - fputs(" = ",e->out); - if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); - } - fputs(";\n",e->out); - } - } - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) { - emit_fn(e,n,1); - if (n->text && strcmp(n->text,"main")==0) main_fn=n; - } - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) { - FeNode *m; - for (m=n->children;m;m=m->next) - if (m->kind==FE_N_FN) emit_fn(e,m,1); - } - fputc('\n',e->out); - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) emit_fn(e,n,0); - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) { - FeNode *m; - for (m=n->children;m;m=m->next) - if (m->kind==FE_N_FN) emit_fn(e,m,0); - } - if (main_fn) { fputc('\n',e->out); emit_main_wrapper(e,main_fn); } -} From 4adfe605742c34ab73e25b5783430c6283a2543b Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:54:17 +0900 Subject: [PATCH 105/184] fix: repair the unified emitter's declarations Compiling the merged sources with the Open Watcom install on this host (C:\WATCOM19\binnt) found four things the merge got wrong, none of which any amount of reading would have caught reliably: - FE_M7_FLOW_CAP and lived in check_m7.c's preamble, above the textual include, and were dropped with the wrapper. - emit_error_return takes a const char *, not a FeNode *; the hand-written forward declaration disagreed with the definition. - emit_match was never defined by the M7 half, only called, so renaming it alongside the other delegating pairs left a declared-but-undefined static. - type_needs_drop and emit_lvalue are used a few hundred lines before the declaration block, so their declarations had to be hoisted. Also drop two locals that existed only to be cast to void. All twelve compiler sources now compile with -za -wx -wcd=202 and produce no warnings. That is a syntax and type check, not verification -- the DOS build and the milestone suite remain the gate. --- fec/check.err | 1 + fec/src/check.c | 3 +++ fec/src/emit_c.c | 19 +++++++++---------- 3 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 fec/check.err diff --git a/fec/check.err b/fec/check.err new file mode 100644 index 0000000..23db97b --- /dev/null +++ b/fec/check.err @@ -0,0 +1 @@ +src\check.c(2795): Error! E1118: ***FATAL*** No such file or directory diff --git a/fec/src/check.c b/fec/src/check.c index 7eaf64a..4a858ed 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -1,5 +1,8 @@ #include "check.h" #include "m7.h" +#include + +#define FE_M7_FLOW_CAP 64U #include "own.h" #include #include diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 53c37d7..318f89e 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -2,6 +2,9 @@ #include #include +static int type_needs_drop(FeType *t); +static void emit_lvalue(FeEmitter *e, FeNode *n); + static void emit_expr_core(FeEmitter *e, FeNode *n); static void emit_stmt_core(FeEmitter *e, FeNode *n); static void emit_block(FeEmitter *e, FeNode *n); @@ -326,15 +329,16 @@ static int node_uses_m4(FeNode *n) static void emit_expr(FeEmitter *e, FeNode *n); static void emit_stmt(FeEmitter *e, FeNode *n); +static void emit_owned_live(FeEmitter *e, FeNode *n, int value); +static void emit_cleanup_all(FeEmitter *e); static void emit_value_drop(FeEmitter *e, FeNode *n); static void emit_cleanup_block(FeEmitter *e, FeNode *n); -static void emit_cleanup_to(FeEmitter *e, unsigned depth); +static void emit_cleanup_to(FeEmitter *e, unsigned floor); static void emit_param_cleanup(FeEmitter *e); -static void emit_error_return(FeEmitter *e, FeNode *n); +static void emit_error_return(FeEmitter *e, const char *error_expr); static void emit_fn(FeEmitter *e, FeNode *fn, int prototype); static void emit_main_wrapper(FeEmitter *e, FeNode *fn); static void emit_type_defs(FeEmitter *e); -static void emit_match(FeEmitter *e, FeNode *n, int value_context); static int stmt_definitely_returns(FeNode *n); @@ -966,7 +970,7 @@ static void emit_expr_core(FeEmitter *e, FeNode *n) -static void emit_match_core(FeEmitter *e, FeNode *n, int value_context) +static void emit_match(FeEmitter *e, FeNode *n, int value_context) { FeNode *arm; FeType *t=n->a ? n->a->sem_type : 0; @@ -1243,7 +1247,6 @@ void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, static void emit_expr(FeEmitter *e, FeNode *n); static void emit_stmt(FeEmitter *e, FeNode *n); static void emit_block(FeEmitter *e, FeNode *n); -static void emit_lvalue(FeEmitter *e, FeNode *n); static int type_needs_drop(FeType *t) { @@ -1394,7 +1397,6 @@ static void m7_emit_drop_helpers(FeEmitter *e) FeType *t; FeNode *method; unsigned i; - unsigned j; char access[256]; for (t=e->check->types.types;t;t=t->next) if (type_needs_drop(t) && t->drop_cname && @@ -1443,7 +1445,6 @@ static void m7_emit_drop_helpers(FeEmitter *e) } /* Error enums are scalar codes, so their enum payload helper functions from M3 are deliberately not emitted in the M7 path. */ - (void)j; } static void m7_emit_type_helpers(FeEmitter *e) @@ -1847,7 +1848,6 @@ static void m7_emit_assign_stmt(FeEmitter *e, const char *dst, FeNode *src) static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) { - FeNode *x; FeType *bt; FeVariantType *v; const char *op; @@ -1987,7 +1987,6 @@ static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) emit_expr_core(e,n); break; } - (void)x; } /* Emit the initializer for a `const` declaration. @@ -2436,7 +2435,7 @@ static void emit_stmt(FeEmitter *e, FeNode *n) case FE_N_MATCH: if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OPTIONAL) m7_emit_optional_match(e,n); - else emit_match_core(e,n,0); + else emit_match(e,n,0); break; case FE_N_BREAK: case FE_N_CONTINUE: From eb85e9fa3d0fbd70bba825bd2ba227111e936424 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 01:57:06 +0900 Subject: [PATCH 106/184] dev: run host syntax gates before starting DOSBox-X There is an Open Watcom install on this host (C:\WATCOM19), and its Windows build compiles the compiler's own sources in about a second. Every declaration mismatch in the unification commits was found that way; each one would otherwise have cost a DOSBox-X boot and a full compiler build to surface, with a DOS-side message that names the wrong thing. Add tools/tests/test_host_syntax.py: compile all twelve sources with the flags build-dos.bat uses (-za -wx -wcd=202) and fail on any diagnostic. It skips when Watcom is absent, so the suite still runs elsewhere. Two structural checks come with it -- that build-dos.bat, the Makefile and the test agree on the source list, and that no .c under fec/src is compiled by nothing. Both would have caught check.c and emit_c.c quietly leaving the build when the M7 wrappers included them textually. ferro-test now runs these and the 8.3 name check before starting the VM, and stops if they fail. This is not verification and does not claim to be: wcc386 targets 32-bit where the real build is 16-bit large model, so it sees syntax, types and declarations and nothing about code generation. The DOS build and the milestone suite remain the gate. It only moves the cheap failures earlier. --- src/ferrolang_vm/test_cli.py | 13 +++- tools/tests/test_host_syntax.py | 105 ++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 tools/tests/test_host_syntax.py diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py index 075a525..d82b2ee 100644 --- a/src/ferrolang_vm/test_cli.py +++ b/src/ferrolang_vm/test_cli.py @@ -62,7 +62,18 @@ def main() -> int: # shell is sitting in -- an editable install plus a git worktree is enough # to silently build and test the wrong tree. Say which tree this is. print(f"ferro-test: building {ROOT}", file=sys.stderr) - test_file = os.fspath(ROOT / "tools" / "tests" / "test_milestones_dosboxx.py") + tests = ROOT / "tools" / "tests" + # The host gates run first and take under a second. A missing declaration + # or an 8.3-illegal name would otherwise be found only after a DOSBox-X + # boot and a full compiler build, and the DOS-side message for either is + # unhelpful. They do not replace the DOS run; they precede it. + gates = [os.fspath(tests / name) for name in + ("test_host_syntax.py", "test_dos_names.py")] + if pytest.main([*gates, "-q", "--no-header"]) != 0: + print("ferro-test: host gates failed; not starting DOSBox-X", + file=sys.stderr) + return 1 + test_file = os.fspath(tests / "test_milestones_dosboxx.py") pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"] if args.select: pytest_args.extend(["-k", args.select]) diff --git a/tools/tests/test_host_syntax.py b/tools/tests/test_host_syntax.py new file mode 100644 index 0000000..56408e5 --- /dev/null +++ b/tools/tests/test_host_syntax.py @@ -0,0 +1,105 @@ +"""Compile the compiler's own sources on the host, as a syntax gate. + +This is not verification. AGENTS.md is explicit that a host compiler's result +does not count, and it still does not: the DOS build and the milestone suite +decide whether anything works. What this buys is the turnaround. A missing +declaration or a signature that disagrees with its definition used to surface +only after a DOSBox-X boot and a full compiler build; here it surfaces in about +a second, with the line number. + +It uses the same Open Watcom the project targets, just the Windows-hosted build, +and the same strictness as ``fec/build-dos.bat`` (``-za -wx -wcd=202``). The +target differs -- wcc386 is 32-bit where the DOS build is 16-bit large model -- +so this catches syntax, types and declarations, not code generation or memory +model problems. + +Skipped when Watcom is not installed, so the suite still runs anywhere. +""" +from __future__ import annotations + +import os +import re +import shutil +import subprocess +from pathlib import Path + +import pytest + +from ferrolang_vm.paths import ROOT + +SRC = ROOT / "fec" / "src" +# Mirrors the compile order in fec/build-dos.bat. +SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", + "check", "lower", "emit_c", "driver") + + +def _watcom() -> Path | None: + root = os.environ.get("WATCOM") + candidates = [Path(root)] if root else [] + candidates.append(Path("C:/WATCOM19")) + for base in candidates: + if (base / "binnt" / "wcc386.exe").is_file(): + return base + return None + + +@pytest.fixture(scope="session") +def watcom() -> Path: + base = _watcom() + if base is None: + pytest.skip("Open Watcom is not installed on the host; set WATCOM to enable") + return base + + +@pytest.fixture(scope="session") +def objdir(tmp_path_factory: pytest.TempPathFactory) -> Path: + return tmp_path_factory.mktemp("wcc") + + +@pytest.mark.parametrize("name", SOURCES) +def test_source_compiles_clean(name: str, watcom: Path, objdir: Path) -> None: + source = SRC / f"{name}.c" + if not source.is_file(): + pytest.fail(f"{source} is missing but build-dos.bat compiles it") + env = dict(os.environ) + env["WATCOM"] = os.fspath(watcom) + env["INCLUDE"] = os.fspath(watcom / "h") + completed = subprocess.run( + [os.fspath(watcom / "binnt" / "wcc386.exe"), "-q", "-za", "-wx", + "-wcd=202", "-zq", f"-i={SRC}", os.fspath(source)], + cwd=objdir, capture_output=True, text=True, env=env, timeout=120, + ) + output = (completed.stdout + completed.stderr).strip() + # -wx keeps warnings meaningful, so treat any diagnostic as a failure: the + # DOS build runs the same flags and stops on them. + assert completed.returncode == 0 and not output, ( + f"{name}.c does not compile clean\n{output}" + ) + + +def test_build_scripts_agree_on_sources() -> None: + """The two build files and this test must name the same translation units. + + They drifted apart while the M7 wrapper existed, which is how a source could + stop being compiled without anyone noticing. + """ + batch = (ROOT / "fec" / "build-dos.bat").read_text(encoding="utf-8", errors="replace") + makefile = (ROOT / "fec" / "Makefile").read_text(encoding="utf-8", errors="replace") + in_batch = set(re.findall(r"src\\(\w+)\.c", batch)) + srcline = next(line for line in makefile.splitlines() if line.startswith("SRC =")) + in_make = set(re.findall(r"src/(\w+)\.c", srcline)) + assert in_batch == set(SOURCES), f"build-dos.bat compiles {sorted(in_batch)}" + assert in_make == set(SOURCES), f"Makefile compiles {sorted(in_make)}" + + +def test_no_source_is_orphaned() -> None: + """Every .c under fec/src must be compiled by something. + + check_m7.c and emitcm7.c hid check.c and emit_c.c from the build by + including them textually; nothing flagged that they had stopped being + translation units of their own. + """ + on_disk = {p.stem for p in SRC.glob("*.c")} + assert on_disk == set(SOURCES), ( + f"fec/src has {sorted(on_disk - set(SOURCES))} that no build step compiles" + ) From ee2b417013dda027ce89410fd04802bcaf13f611 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 02:07:26 +0900 Subject: [PATCH 107/184] fix: restore the rules the M7 half never had to implement Unifying the two engines exposed what the split had been hiding: every rule that lived only in the M6 body was silently dropped for units the M7 half claimed, and since M1-M6 sources never reached that half, nothing failed until they all did. Eleven cases across m3, m5 and m6 caught it. Checker, all from the M6 statement and lvalue cases: - writing a struct field needs a writable place, so `p.x = 3` on a `let` is an error again (m3-badfield) - `let` cannot bind a mutable slice, a var with no initializer needs a type, and a void expression cannot initialize (m3-bad-mlet) - a returned reference must derive from a parameter or a static, and a void expression cannot be returned from a value function (m6-badarg, badret, badself, badtwo, badlocsl) - rebinding a reference must not outlive its source scope, and must release the previous borrow (m6-badscop) - the loop case delegates to the core, which carries the flow capture and merge that detects a value moved on every iteration; the M7 version had none of it (m5-bad-loop) Emitter: - builtins other than the print family (@size_of, @align_of) and the str alias methods are lowered by the core, which the M7 call path never reached, so they were emitted verbatim into the C (m3-struct) - the trim helper is emitted from the M7 type-helper pass as well, not only the core one, or the call has no definition to link (m6-oktrim) M1-M7 all green: 16, 19, 50, 20, 16, 57, 42. --- fec/src/check.c | 59 +++++++++++++++++++++++++++++++++++++++--------- fec/src/emit_c.c | 22 ++++++++++++++++++ 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index 4a858ed..141d443 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -2247,6 +2247,12 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { if (base->kind==FE_TYPE_REF && !base->ref_mut) err(s->c,n->loc,"cannot write through shared reference"); + /* Writing a field still needs a writable place. This branch used to + be reached only by units mentioning M7 syntax, so it never had to + repeat the check the M6 path does. */ + if (base->kind!=FE_TYPE_REF && base->kind!=FE_TYPE_OWNED && + !lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); field=fe_type_field(owner,n->b->text); if (!field) { err(s->c,n->loc,"assignment requires a valid struct field"); @@ -2485,6 +2491,15 @@ static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) if (n->b && !fe_type_equal(expected,stored) && !m7_actual_compatible(expected,stored,n->b)) err(s->c,n->loc,"initializer type mismatch"); + /* Rules the M6 declaration case carried that this one has to repeat now + that it is the only declaration case. */ + if (n->b && stored && stored->kind==FE_TYPE_VOID) + err(s->c,n->loc,"void expression cannot initialize a variable"); + if (!mutable && expected && expected->kind==FE_TYPE_SLICE && + expected->ref_mut) + err(s->c,n->loc,"let cannot bind a mutable slice"); + if (!n->b && !n->a) + err(s->c,n->loc,"uninitialized var requires an explicit type"); if (n->b) mark_moved(s,n->b,actual); initialized=n->b!=0; sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, @@ -2542,6 +2557,25 @@ static void check_stmt(FeCheckerState *s, FeNode *n) sym->initialized=1; fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); sym->moved=sym->own.move; + /* Rebinding a reference, from the M6 assignment case: the new + source has to live at least as long as the reference does, and + the previous borrow has to be released. */ + if (n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && + fe_own_is_reference_like(sym->type)) { + FeSym *root=own_root_symbol(s,n->b->a); + if (root && root->owner!=sym->owner) + err(s->c,n->b->loc,"reference would outlive its source scope"); + else if (root) { + if (sym->borrow_root) { + if (sym->borrow_mut) + fe_own_release_exclusive(&sym->borrow_root->own); + else fe_own_release_shared(&sym->borrow_root->own); + } + sym->borrow_root=root; + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + } + } } break; case FE_N_EXPR_STMT: @@ -2614,6 +2648,15 @@ static void check_stmt(FeCheckerState *s, FeNode *n) else stored=fe_type_intern(&s->c->types,"void"); actual=n->a && n->a->sem_type ? n->a->sem_type : stored; + /* R8, from the M6 return case: a returned reference has to come from a + parameter or a static, never from a local. */ + if (expected && fe_own_is_reference_like(expected) && + !own_return_from_allowed_root(s,n->a)) + err(s->c,n->loc, + "reference return must be derived from a parameter or static"); + if (n->a && stored && stored->kind==FE_TYPE_VOID && + expected && expected->kind!=FE_TYPE_VOID) + err(s->c,n->loc,"void expression returned from value function"); if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a && actual && actual->kind==FE_TYPE_ERROR_UNION && !fe_type_equal(expected,actual)) @@ -2625,17 +2668,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n) break; case FE_N_WHILE: case FE_N_FOR: - /* One loop rule now that there is one checker: the body recurses - through this function, so any expression in it is checked the same - way whether or not the unit mentions optionals or error unions. */ - if (n->kind==FE_N_WHILE) { - actual=check_expr(s,n->a); - if (known(actual) && actual->kind!=FE_TYPE_BOOL) - err(s->c,n->loc,"while condition must be bool"); - ++s->loop_depth; - check_stmt(s,n->b); - --s->loop_depth; - } else check_for(s,n); + /* The core loop case carries the flow capture and merge that detects a + value moved on every iteration, and it already recurses into the body + through this function, so there is nothing to special-case here. The + M7 half used to skip all of it. */ + check_stmt_core(s,n); break; case FE_N_BREAK: case FE_N_CONTINUE: diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index 318f89e..b168c39 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -1580,6 +1580,14 @@ static void m7_emit_type_helpers(FeEmitter *e) m7_c_type(e,t),t->full_slicer,m7_c_type(e,t),t->slicer); fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n", m7_c_type(e,t),t->tail_slicer,m7_c_type(e,t),t->slicer); + if (node_uses_trim(e->check->ast->root) && !t->ref_mut) { + fprintf(e->out, + "static %s fe_trim_%s(%s s) { unsigned long a=0; unsigned long b=s.n;" + " while (aa && (s.p[b-1]==' '||s.p[b-1]=='\\t'||s.p[b-1]=='\\r'||s.p[b-1]=='\\n')) --b;" + " return %s(s.p+a,b-a); }\n", + t->cname,t->cname,t->cname,t->maker); + } } } } @@ -1778,6 +1786,20 @@ static void m7_emit_call(FeEmitter *e, FeNode *n) emit_m4_builtin(e,n); return; } + /* Every other builtin -- @size_of, @align_of and friends -- is lowered by + the core emitter. Without this the generic path below emits the call + verbatim, which is not C. */ + if (!n->a && n->text && n->text[0]=='@') { + emit_expr_core(e,n); + return; + } + /* Same for the built-in alias methods on str: the core emitter knows how to + lower `line.trim()`, the generic member path would emit `.trim()`. */ + if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"trim")==0 && !n->children) { + emit_expr_core(e,n); + return; + } if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->kind==FE_N_IDENT && n->a->a->text && strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && From 32ad50b14a78a28320a1abbdca76f3c0da5a3bb6 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 02:22:44 +0900 Subject: [PATCH 108/184] dev: run the host gate as 16-bit, and drop what it found dead The gate was using a system-wide Open Watcom that only ships the 32-bit compiler. The project downloads its own toolchain, and .dosboxx/watcom/binnt has the Windows-hosted 16-bit wcl.exe -- the same compiler and the same target as the DOS build. Run that instead, with build-dos.bat's exact command. The difference is not academic. Compiling 16-bit immediately reported three functions the unification had orphaned: emit_type_helpers, which the M7 program emitter replaced, and emit_drop_helpers and emit_drop_fields, which it was the only caller of. The 32-bit check had been clean. Those warnings were going to the DOS screen, where the runner cannot see them: COMMAND.COM redirects handle 1 only, and Watcom writes diagnostics to handle 2. So the suite was green while the build was not quiet. Nothing in the runner would ever have said so. Found while chasing W210/W107 reported from a DOS screen, which this does not yet explain -- those are not among what the compiler build emits now. M1-M7: 214 passed. --- fec/src/emit_c.c | 157 -------------------------------- tools/tests/test_host_syntax.py | 38 +++++--- 2 files changed, 24 insertions(+), 171 deletions(-) diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c index b168c39..1fc185b 100644 --- a/fec/src/emit_c.c +++ b/fec/src/emit_c.c @@ -111,74 +111,7 @@ static FeNode *find_drop_method(FeEmitter *e, const char *name) return 0; } -static void emit_drop_fields(FeEmitter *e, FeType *t) -{ - unsigned i; - FeType *ft; - for (i=t->field_count; i>0; --i) { - ft=t->fields[i-1].type; - if (!type_needs_drop(ft)) continue; - if (ft->kind==FE_TYPE_OWNED) { - fputs("if (self->",e->out); fputs(t->fields[i-1].name,e->out); - if(ft->elem && ft->elem->kind==FE_TYPE_SLICE) fputs(".p",e->out); - fputs(") { ",e->out); - if(ft->elem && ft->elem->kind==FE_TYPE_SLICE) { - fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs(".p); self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs(".p=0; ",e->out); - } else if (ft->elem && type_needs_drop(ft->elem) && ft->elem->drop_cname) { - fprintf(e->out,"%s(self->%s); ",ft->elem->drop_cname,t->fields[i-1].name); - } - if(!(ft->elem && ft->elem->kind==FE_TYPE_SLICE)) { - fputs("free(self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs("); self->",e->out); fputs(t->fields[i-1].name,e->out); - fputs("=0; ",e->out); - } - fputs("}\n",e->out); - } else if (ft->kind==FE_TYPE_STRUCT && ft->drop_cname) { - fprintf(e->out,"%s(&self->%s);\n",ft->drop_cname,t->fields[i-1].name); - } else if (ft->kind==FE_TYPE_ARRAY && ft->drop_cname) { - fprintf(e->out,"%s(&self->%s);\n",ft->drop_cname,t->fields[i-1].name); - } - } -} -static void emit_drop_helpers(FeEmitter *e) -{ - FeType *t; - FeNode *method; - for (t=e->check->types.types; t; t=t->next) - if(t->kind==FE_TYPE_STRUCT && (method=find_drop_method(e,t->name))!=0) - fprintf(e->out,"void %s(%s *self);\n", - cname(method,"fe_drop_method"),t->cname); - for (t=e->check->types.types; t; t=t->next) - if ((t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY) && - type_needs_drop(t) && t->drop_cname) - fprintf(e->out,"static void %s(%s *self);\n",t->drop_cname,t->cname); - for (t=e->check->types.types; t; t=t->next) { - if (t->kind!=FE_TYPE_STRUCT || !type_needs_drop(t) || !t->drop_cname) continue; - fprintf(e->out,"static void %s(%s *self) {\n",t->drop_cname,t->cname); - method=find_drop_method(e,t->name); - if (method) { - fprintf(e->out,"%s(self);\n",cname(method,"fe_drop_method")); - } - emit_drop_fields(e,t); - fputs("}\n",e->out); - } - for (t=e->check->types.types; t; t=t->next) - if (t->kind==FE_TYPE_ARRAY && type_needs_drop(t) && t->drop_cname) { - fprintf(e->out,"static void %s(%s *self) { unsigned long i; for (i=0; i<%lu; ++i) { ", - t->drop_cname,t->cname,t->length); - if (t->elem->kind==FE_TYPE_OWNED) { - fputs("if (self->a[i]) { ",e->out); - if (t->elem->elem && type_needs_drop(t->elem->elem) && t->elem->elem->drop_cname) - fprintf(e->out,"%s(self->a[i]); ",t->elem->elem->drop_cname); - fputs("free(self->a[i]); self->a[i]=0; }",e->out); - } else if (t->elem->drop_cname) - fprintf(e->out,"%s(&self->a[i]);",t->elem->drop_cname); - fputs(" } }\n",e->out); - } -} /* SPEC 12.3 lists `trim` among the built-in alias methods on `str`, called as `line.trim()`. The checker accepts it; this emits the lowering. Only the @@ -196,96 +129,6 @@ static int node_uses_trim(FeNode *n) return 0; } -static void emit_type_helpers(FeEmitter *e) -{ - FeType *t; - unsigned i,j; - for(t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && - t->error_value->kind!=FE_TYPE_VOID) { - fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", - t->cname,t->maker,fe_type_c_name(t->error_value,e->pointer_bits),t->cname); - if (t->error_value->kind==FE_TYPE_OWNED && - t->error_value->elem && - t->error_value->elem->kind==FE_TYPE_SLICE) { - FeType *item=t->error_value->elem->elem; - fprintf(e->out,"static %s %s(unsigned long n) { %s r; r.v.p=(%s*)malloc(sizeof(%s)*n); r.v.n=n; r.e=(r.v.p || !n) ? 0 : 1; return r; }\n", - t->cname,t->alloc_cname,t->cname, - fe_type_c_name(item,e->pointer_bits), - fe_type_c_name(item,e->pointer_bits)); - } else if (t->error_value->kind==FE_TYPE_OWNED) { - fprintf(e->out,"static %s %s(%s v) { %s r; r.v=(%s)malloc(sizeof(%s)); if(r.v) *r.v=v; r.e=r.v ? 0 : 1; return r; }\n", - t->cname,t->alloc_cname, - fe_type_c_name(t->error_value->elem,e->pointer_bits),t->cname, - fe_type_c_name(t->error_value,e->pointer_bits), - fe_type_c_name(t->error_value->elem,e->pointer_bits)); - } - } - } - for(t=e->check->types.types;t;t=t->next) { - if(t->replace_cname) { - const char *ct=fe_type_c_name(t,e->pointer_bits); - fprintf(e->out,"static %s %s(%s *dst, %s value) { %s old=*dst; *dst=value; return old; }\n", - ct,t->replace_cname,ct,ct,ct); - } - if(t->kind==FE_TYPE_STRUCT && t->maker) { - fprintf(e->out,"static %s %s(",t->cname,t->maker); - for(i=0;ifield_count;i++) { if(i) fputs(", ",e->out); fputs(fe_type_c_name(t->fields[i].type,e->pointer_bits),e->out); fprintf(e->out," p%u",i); } - fputs(") { ",e->out); fprintf(e->out,"%s v;",t->cname); - for(i=0;ifield_count;i++) fprintf(e->out," v.%s=p%u;",t->fields[i].name,i); - fputs(" return v; }\n",e->out); - } else if(t->kind==FE_TYPE_ARRAY && t->maker) { - fprintf(e->out,"static %s %s(",t->cname,t->maker); - for(i=0;ilength;i++) { if(i) fputs(", ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fprintf(e->out," p%u",i); } - fputs(") { ",e->out); fprintf(e->out,"%s v;",t->cname); - for(i=0;ilength;i++) fprintf(e->out," v.a[%u]=p%u;",i,i); - fputs(" return v; }\n",e->out); - } else if(t->kind==FE_TYPE_ENUM) { - for(i=0;ivariant_count;i++) { - FeVariantType *v=&t->variants[i]; - fprintf(e->out,"static %s %s(",t->cname,v->maker); - for(j=0;jfield_count;j++) { if(j) fputs(", ",e->out); fputs(fe_type_c_name(v->fields[j].type,e->pointer_bits),e->out); fprintf(e->out," p%u",j); } - fputs(") { ",e->out); fprintf(e->out,"%s x; x.tag=%u;",t->cname,v->tag); - for(j=0;jfield_count;j++) { if(v->field_count==1) fprintf(e->out," x.payload.%s=p%u;",v->name,j); else fprintf(e->out," x.payload.%s.%s=p%u;",v->name,v->fields[j].name,j); } - fputs(" return x; }\n",e->out); - } - } - } - emit_drop_helpers(e); - for(t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_ARRAY && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); - if(!e->no_checks) fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); - fprintf(e->out,"return x.a[i]; }\n"); - fprintf(e->out,"static %s %s(%s *x, unsigned long a, unsigned long b) { ", - fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->slicer,t->cname); - if(!e->no_checks) fputs("if (a > b || b > ",e->out), fprintf(e->out,"%lu",t->length), fputs(") fe_trap_bounds(); ",e->out); - fprintf(e->out,"return %s(x->a+a,b-a); }\n",fe_type_slice(&e->check->types,t->elem)->maker); - fprintf(e->out,"static %s %s(%s *x) { return %s(x,0,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->full_slicer,t->cname,t->slicer,t->length); - fprintf(e->out,"static %s %s(%s *x, unsigned long a) { return %s(x,a,%lu); }\n",fe_type_c_name(fe_type_slice(&e->check->types,t->elem),e->pointer_bits),t->tail_slicer,t->cname,t->slicer,t->length); - } else if (t->kind==FE_TYPE_SLICE && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - fe_type_c_name(t->elem,e->pointer_bits),t->indexer,t->cname); - if(!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); - fputs("return x.p[i]; }\n",e->out); - fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", - fe_type_c_name(t,e->pointer_bits),t->slicer,t->cname); - if(!e->no_checks) fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); - fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker); - fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n",t->cname,t->full_slicer,t->cname,t->slicer); - fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n",t->cname,t->tail_slicer,t->cname,t->slicer); - if (node_uses_trim(e->check->ast->root) && !t->ref_mut) { - fprintf(e->out, - "static %s fe_trim_%s(%s s) { unsigned long a=0; unsigned long b=s.n;" - " while (aa && (s.p[b-1]==' '||s.p[b-1]=='\\t'||s.p[b-1]=='\\r'||s.p[b-1]=='\\n')) --b;" - " return %s(s.p+a,b-a); }\n", - t->cname,t->cname,t->cname,t->maker); - } - } - } -} static void emit_m4_runtime(FeEmitter *e) { diff --git a/tools/tests/test_host_syntax.py b/tools/tests/test_host_syntax.py index 56408e5..8fb4972 100644 --- a/tools/tests/test_host_syntax.py +++ b/tools/tests/test_host_syntax.py @@ -7,13 +7,17 @@ declaration or a signature that disagrees with its definition used to surface only after a DOSBox-X boot and a full compiler build; here it surfaces in about a second, with the line number. -It uses the same Open Watcom the project targets, just the Windows-hosted build, -and the same strictness as ``fec/build-dos.bat`` (``-za -wx -wcd=202``). The -target differs -- wcc386 is 32-bit where the DOS build is 16-bit large model -- -so this catches syntax, types and declarations, not code generation or memory -model problems. +It runs the pinned toolchain's Windows-hosted 16-bit driver with the exact +command ``fec/build-dos.bat`` uses, so the diagnostics match what the DOS build +sees. A 32-bit compile is not equivalent: it misses warnings that only the +16-bit large model reports, which is how a dead function survived the M7 +unification with a clean 32-bit check. -Skipped when Watcom is not installed, so the suite still runs anywhere. +What it still cannot see is the DOS environment itself -- memory limits, the +command line length, the filesystem. The DOS build and the milestone suite +remain the gate. + +Skipped when the toolchain has not been downloaded, so the suite runs anywhere. """ from __future__ import annotations @@ -34,11 +38,13 @@ SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", def _watcom() -> Path | None: + """Prefer the toolchain the runner pins over anything installed system-wide.""" root = os.environ.get("WATCOM") - candidates = [Path(root)] if root else [] - candidates.append(Path("C:/WATCOM19")) + candidates = [ROOT / ".dosboxx" / "watcom"] + if root: + candidates.append(Path(root)) for base in candidates: - if (base / "binnt" / "wcc386.exe").is_file(): + if (base / "binnt" / "wcl.exe").is_file(): return base return None @@ -47,7 +53,7 @@ def _watcom() -> Path | None: def watcom() -> Path: base = _watcom() if base is None: - pytest.skip("Open Watcom is not installed on the host; set WATCOM to enable") + pytest.skip("run `ferro-dos setup` to download the pinned toolchain") return base @@ -64,14 +70,18 @@ def test_source_compiles_clean(name: str, watcom: Path, objdir: Path) -> None: env = dict(os.environ) env["WATCOM"] = os.fspath(watcom) env["INCLUDE"] = os.fspath(watcom / "h") + env["PATH"] = os.pathsep.join( + [os.fspath(watcom / "binnt"), env.get("PATH", "")]) + # The same command build-dos.bat runs, minus the object name. completed = subprocess.run( - [os.fspath(watcom / "binnt" / "wcc386.exe"), "-q", "-za", "-wx", - "-wcd=202", "-zq", f"-i={SRC}", os.fspath(source)], + [os.fspath(watcom / "binnt" / "wcl.exe"), "-q", "-za", "-wx", + "-bt=dos", "-ml", "-k32768", "-c", f"-i={SRC}", os.fspath(source)], cwd=objdir, capture_output=True, text=True, env=env, timeout=120, ) output = (completed.stdout + completed.stderr).strip() - # -wx keeps warnings meaningful, so treat any diagnostic as a failure: the - # DOS build runs the same flags and stops on them. + # -wx keeps warnings meaningful, so treat any diagnostic as a failure. The + # DOS build prints them to a screen nobody reads, which is how they + # accumulate unnoticed. assert completed.returncode == 0 and not output, ( f"{name}.c does not compile clean\n{output}" ) From d5ba699744e6a434dfd728ff45cc12b868c7c715 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 02:26:02 +0900 Subject: [PATCH 109/184] dev: require the pinned toolchain, and clear the QEMU-era leftovers The host gate accepted a WATCOM environment override and skipped when nothing was found. Both are wrong for what it is: a system-wide Open Watcom is a different version reporting different diagnostics, and a gate that skips is a gate that is not running, which is the exact shape of the problem this file was added to close. It now uses .dosboxx/watcom only and fails with the setup command when that is absent, matching how dosboxx.py already behaves. Nothing else in the project reaches for a system install: the DOS session sets WATCOM=W: before calling BUILD.BAT, so the C:\DEVEL\WATCOMC fallback inside it is unreachable. fec/test-dos.bat was tracked but dead -- the runner generates RUN.BAT and only copies build-dos.bat -- so it goes, along with the comment claiming it drives the build and the three fixture READMEs that still pointed at it. The registry decides what runs now. (.qemu/ is untracked local debris from the same era and is left alone.) --- fec/build-dos.bat | 2 +- fec/test-dos.bat | 288 -------------------------------- fec/tests/m6/README.md | 2 +- fec/tests/m7/README.md | 2 +- fec/tests/m8/README.md | 2 +- tools/tests/test_host_syntax.py | 32 ++-- 6 files changed, 19 insertions(+), 309 deletions(-) delete mode 100644 fec/test-dos.bat diff --git a/fec/build-dos.bat b/fec/build-dos.bat index 733cbea..e8c1178 100644 --- a/fec/build-dos.bat +++ b/fec/build-dos.bat @@ -1,5 +1,5 @@ @echo off -rem Open Watcom C89 build. TEST-DOS.BAT runs this from C:\FEC. +rem Open Watcom C89 build. The runner's generated RUN.BAT calls this from C:\FEC. C: cd \FEC if exist BUILD.OK del BUILD.OK diff --git a/fec/test-dos.bat b/fec/test-dos.bat deleted file mode 100644 index bdd2d15..0000000 --- a/fec/test-dos.bat +++ /dev/null @@ -1,288 +0,0 @@ -@echo off -rem FreeDOS smoke tests. All work happens on the writable C: drive. -C: -cd \FEC -if exist TEST.OK del TEST.OK -if exist TEST.FAIL del TEST.FAIL -call C:\FEC\BUILD.BAT -if not exist BUILD.OK goto test_fail -if "%WATCOM%"=="" set WATCOM=C:\DEVEL\WATCOMC -if not exist %WATCOM%\BINW\WCL.EXE goto test_fail -if not exist %WATCOM%\BINW\WCL386.EXE goto test_fail -set PATH=%WATCOM%\BINW;%WATCOM%\BINP;%PATH% - -fec.exe --dump-ast TESTS\PASS\BASIC.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\PASS\LITERALS.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\PASS\KEYBUILT.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\PASS\V012FORM.FE > nul -if errorlevel 1 goto test_fail - -fec.exe --dump-ast STD\CORE.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast STD\FMT.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast STD\IO.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast STD\LIST.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast STD\MAP.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast STD\MEM.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast STD\STR.FE > nul -if errorlevel 1 goto test_fail -fec.exe --dump-ast STD\SYS.FE > nul -if errorlevel 1 goto test_fail - -fec.exe --dump-ast TESTS\FAIL\MISSSEMI.FE > nul -if not errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\UNCLCOMM.FE > nul -if not errorlevel 1 goto test_fail -fec.exe --dump-ast TESTS\FAIL\LOGICAL.FE > nul -if not errorlevel 1 goto test_fail - -if exist TESTS\M2\HELLO.C del TESTS\M2\HELLO.C -if exist TESTS\M2\HELLO.EXE del TESTS\M2\HELLO.EXE -if exist TESTS\M2\SCOPES.C del TESTS\M2\SCOPES.C -if exist TESTS\M2\SCOPES.EXE del TESTS\M2\SCOPES.EXE -if exist TESTS\M2\CAST16.C del TESTS\M2\CAST16.C -if exist TESTS\M2\CAST16.EXE del TESTS\M2\CAST16.EXE - -rem M2 bits32 path: Open Watcom 32-bit compiler and DOS extender executable. -fec.exe --target=bits32 --emit-c TESTS\M2\HELLO.FE -o TESTS\M2\HELLO.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M2\HELLO.EXE TESTS\M2\HELLO.C -if errorlevel 1 goto test_fail -TESTS\M2\HELLO.EXE -if errorlevel 1 goto test_fail - -fec.exe --target=bits32 --emit-c TESTS\M2\SCOPES.FE -o TESTS\M2\SCOPES.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M2\SCOPES.EXE TESTS\M2\SCOPES.C -if errorlevel 1 goto test_fail -TESTS\M2\SCOPES.EXE -if errorlevel 1 goto test_fail - -rem M2 bits16 regression path remains on compiler A (wcl). -fec.exe --target=bits16 --emit-c TESTS\M2\CASTWHIL.FE -o TESTS\M2\CAST16.C > nul -if errorlevel 1 goto test_fail -wcl -q -za -bt=dos -fe=TESTS\M2\CAST16.EXE TESTS\M2\CAST16.C -if errorlevel 1 goto test_fail -TESTS\M2\CAST16.EXE -if errorlevel 1 goto test_fail - -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-COND.FE -o TESTS\M2\BAD-CO.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-CAST.FE -o TESTS\M2\BAD-CA.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ASGN.FE -o TESTS\M2\BAD-AS.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNK.FE -o TESTS\M2\BAD-UN.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-ARI.FE -o TESTS\M2\BAD-AR.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-TYPE.FE -o TESTS\M2\BAD-TY.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-RET.FE -o TESTS\M2\BAD-RE.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-UNIT.FE -o TESTS\M2\BAD-UI.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M2\BAD-VOID.FE -o TESTS\M2\BAD-VO.C > nul -if not errorlevel 1 goto test_fail - -if exist TESTS\M3\STRUCT.C del TESTS\M3\STRUCT.C -if exist TESTS\M3\STRUCT.EXE del TESTS\M3\STRUCT.EXE -if exist TESTS\M3\ENUM.C del TESTS\M3\ENUM.C -if exist TESTS\M3\ENUM.EXE del TESTS\M3\ENUM.EXE -if exist TESTS\M3\ARRAY.C del TESTS\M3\ARRAY.C -if exist TESTS\M3\ARRAY.EXE del TESTS\M3\ARRAY.EXE -if exist TESTS\M3\STR.C del TESTS\M3\STR.C -if exist TESTS\M3\STR.EXE del TESTS\M3\STR.EXE -if exist TESTS\M3\FOR.C del TESTS\M3\FOR.C -if exist TESTS\M3\FOR.EXE del TESTS\M3\FOR.EXE -if exist TESTS\M3\NESTED.C del TESTS\M3\NESTED.C -if exist TESTS\M3\NESTED.EXE del TESTS\M3\NESTED.EXE -if exist TESTS\M3\CHAR.C del TESTS\M3\CHAR.C -if exist TESTS\M3\CHAR.EXE del TESTS\M3\CHAR.EXE -if exist TESTS\M3\ARRAYCTX.C del TESTS\M3\ARRAYCTX.C -if exist TESTS\M3\ARRAYCTX.EXE del TESTS\M3\ARRAYCTX.EXE -if exist TESTS\M3\BOUNDS.C del TESTS\M3\BOUNDS.C -if exist TESTS\M3\BOUNDS.EXE del TESTS\M3\BOUNDS.EXE -if exist TESTS\M3\BOUNDS-N.C del TESTS\M3\BOUNDS-N.C -if exist TESTS\M3\BOUNDS-N.EXE del TESTS\M3\BOUNDS-N.EXE - -fec.exe --target=bits32 --emit-c TESTS\M3\STRUCT.FE -o TESTS\M3\STRUCT.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\STRUCT.EXE TESTS\M3\STRUCT.C -if errorlevel 1 goto test_fail -TESTS\M3\STRUCT.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\ENUM.FE -o TESTS\M3\ENUM.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\ENUM.EXE TESTS\M3\ENUM.C -if errorlevel 1 goto test_fail -TESTS\M3\ENUM.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\ARRAY.FE -o TESTS\M3\ARRAY.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\ARRAY.EXE TESTS\M3\ARRAY.C -if errorlevel 1 goto test_fail -TESTS\M3\ARRAY.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\MUTABLE.FE -o TESTS\M3\MUTABLE.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\MUTABLE.EXE TESTS\M3\MUTABLE.C -if errorlevel 1 goto test_fail -TESTS\M3\MUTABLE.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BAD-MLET.FE -o TESTS\M3\BAD-MLET.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BAD-SHWR.FE -o TESTS\M3\BAD-SHWR.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\STR.FE -o TESTS\M3\STR.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\STR.EXE TESTS\M3\STR.C -if errorlevel 1 goto test_fail -TESTS\M3\STR.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\FOR.FE -o TESTS\M3\FOR.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\FOR.EXE TESTS\M3\FOR.C -if errorlevel 1 goto test_fail -TESTS\M3\FOR.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\NESTED.FE -o TESTS\M3\NESTED.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\NESTED.EXE TESTS\M3\NESTED.C -if errorlevel 1 goto test_fail -TESTS\M3\NESTED.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\CHAR.FE -o TESTS\M3\CHAR.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\CHAR.EXE TESTS\M3\CHAR.C -if errorlevel 1 goto test_fail -TESTS\M3\CHAR.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\ARRAYCTX.FE -o TESTS\M3\ARRAYCTX.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\ARRAYCTX.EXE TESTS\M3\ARRAYCTX.C -if errorlevel 1 goto test_fail -TESTS\M3\ARRAYCTX.EXE -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BOUNDS.FE -o TESTS\M3\BOUNDS.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\BOUNDS.EXE TESTS\M3\BOUNDS.C -if errorlevel 1 goto test_fail -TESTS\M3\BOUNDS.EXE -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\SLCBOUND.FE -o TESTS\M3\SLCBOUND.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\SLCBOUND.EXE TESTS\M3\SLCBOUND.C -if errorlevel 1 goto test_fail -TESTS\M3\SLCBOUND.EXE -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --no-checks --emit-c TESTS\M3\BOUNDS.FE -o TESTS\M3\BOUNDS-N.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -bt=dos -fe=TESTS\M3\BOUNDS-N.EXE TESTS\M3\BOUNDS-N.C -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADFLD.FE -o TESTS\M3\BADFLD.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADMAT.FE -o TESTS\M3\BADMAT.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADARR.FE -o TESTS\M3\BADARR.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADCYCLE.FE -o TESTS\M3\BADCYCLE.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADSTR.FE -o TESTS\M3\BADSTR.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADCHAR.FE -o TESTS\M3\BADCHAR.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADFIELD.FE -o TESTS\M3\BADFIELD.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M3\BADINDEX.FE -o TESTS\M3\BADINDEX.C > nul -if not errorlevel 1 goto test_fail - -if exist TESTS\M4\FORMAT.C del TESTS\M4\FORMAT.C -if exist TESTS\M4\FORMAT.EXE del TESTS\M4\FORMAT.EXE -fec.exe --target=bits32 --emit-c TESTS\M4\FORMAT.FE -o TESTS\M4\FORMAT.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\FORMAT.EXE TESTS\M4\FORMAT.C -if errorlevel 1 goto test_fail -TESTS\M4\FORMAT.EXE > nul -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\TRY-FPR.FE -o TESTS\M4\TRY-FPR.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\TRY-FPR.EXE TESTS\M4\TRY-FPR.C -if errorlevel 1 goto test_fail -TESTS\M4\TRY-FPR.EXE > nul -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\PROP.FE -o TESTS\M4\PROP.C > nul -if errorlevel 1 goto test_fail -wcl386 -q -za -wx -wcd=202 -bt=dos -fe=TESTS\M4\PROP.EXE TESTS\M4\PROPTEST.C -if errorlevel 1 goto test_fail -TESTS\M4\PROP.EXE > nul -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-ARI.FE -o TESTS\M4\BAD-ARI.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-VERB.FE -o TESTS\M4\BAD-VERB.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-RUN.FE -o TESTS\M4\BAD-RUN.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TYPE.FE -o TESTS\M4\BAD-TYP.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-TRY.FE -o TESTS\M4\BAD-TRY.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-WRIT.FE -o TESTS\M4\BAD-WRI.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-BUFW.FE -o TESTS\M4\BAD-BUFW.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-MANY.FE -o TESTS\M4\BAD-MANY.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-OPEN.FE -o TESTS\M4\BAD-OPEN.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M4\BAD-CLS.FE -o TESTS\M4\BAD-CLS.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\DEFER.FE -o TESTS\M5\DEFER.C > nul -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\OWNED.FE -o TESTS\M5\OWNED.C > nul -if errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-MOVE.FE -o TESTS\M5\BAD-MOVE.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DEST.FE -o TESTS\M5\BAD-DES.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DROP.FE -o TESTS\M5\BAD-DROP.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-DBL.FE -o TESTS\M5\BAD-DBL.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-COND.FE -o TESTS\M5\BAD-COND.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-PROJ.FE -o TESTS\M5\BAD-PROJ.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-CLOS.FE -o TESTS\M5\BAD-CLOS.C > nul -if not errorlevel 1 goto test_fail -fec.exe --target=bits32 --emit-c TESTS\M5\BAD-LOOP.FE -o TESTS\M5\BAD-LOOP.C > nul -if not errorlevel 1 goto test_fail -if exist TESTS\M5\RUNT-G.C del TESTS\M5\RUNT-G.C -if exist TESTS\M5\RUNTIME.O del TESTS\M5\RUNTIME.O -if exist TESTS\M5\RUNTIME.EXE del TESTS\M5\RUNTIME.EXE -fec.exe --target=bits32 --emit-c TESTS\M5\RUNTIME.FE -o TESTS\M5\RUNT-G.C > nul -if errorlevel 1 goto test_fail -rem Compile generated source and the C89 runtime harness in one WCL386 invocation -rem so both objects use the same DOS/4GW startup and runtime library. -wcl386 -q -za -bt=dos -dmalloc=m5_malloc -dfree=m5_free -fe=TESTS\M5\RUNTIME.EXE TESTS\M5\RUNT-G.C TESTS\M5\RUNTIME.C -if errorlevel 1 goto test_fail -TESTS\M5\RUNTIME.EXE -if errorlevel 1 goto test_fail - -echo OK>TEST.OK -cd C:\FEC -goto test_done - -:test_fail -echo FAIL>TEST.FAIL -verify other 2>nul - -:test_done diff --git a/fec/tests/m6/README.md b/fec/tests/m6/README.md index 743005f..3d0fcd7 100644 --- a/fec/tests/m6/README.md +++ b/fec/tests/m6/README.md @@ -4,7 +4,7 @@ These fixtures pin the M6 ownership/borrow rules before implementation. - `ok*.fe` must compile with `--target=bits32 --emit-c`. - `bad*.fe` must fail compilation; the first line follows the `// ERROR::` convention from SPEC §12. -- They are intentionally not wired into `TEST-DOS.BAT` while `master` is M5, so adding these fixtures does not make the current milestone red. +- They are registered in `src/ferrolang_vm/registry.py`, which is what decides whether a milestone runs. - When M6 starts, wire this directory into the DOS/QEMU gate without changing the expected result of any fixture. - M6 also owns the general-global borrow restriction from R10 because AGENTS.md explicitly groups that change with the `own.c` state-machine work. diff --git a/fec/tests/m7/README.md b/fec/tests/m7/README.md index 3679b4b..237a2e6 100644 --- a/fec/tests/m7/README.md +++ b/fec/tests/m7/README.md @@ -3,6 +3,6 @@ M7 adds optionals and error unions on top of the M6 ownership model. `ok*.fe` must compile. `bad*.fe` must fail according to the first-line error marker. -The files are not wired into `TEST-DOS.BAT` until M7 work begins. +The files are registered in `src/ferrolang_vm/registry.py`. Coverage: contextual `null`, `?T`, `.?`, `Some`/`None` pattern-only non-destructive views, `mem.replace` extraction, lazy `orelse`/`catch`, nominal error unions, `try`, block/short `catch`, and error code/name uniqueness, plus R4/R7 interactions with optional references/owners. diff --git a/fec/tests/m8/README.md b/fec/tests/m8/README.md index 6271483..c5ac55b 100644 --- a/fec/tests/m8/README.md +++ b/fec/tests/m8/README.md @@ -21,6 +21,6 @@ Fail cases: - `pubpriv`: a public signature cannot expose a private nominal type. - `errnom`: nominal error cannot flow into `core.Error` via `try`. -The current M5 `TEST-DOS.BAT` is intentionally unchanged. M8 should add procedural checks for `.fei` creation/hash invalidation and deterministic `fe_errors.h` using these fixtures. +They are not registered in `src/ferrolang_vm/registry.py` yet. M8 should add procedural checks for `.fei` creation/hash invalidation and deterministic `fe_errors.h` using these fixtures. The M8 DOS gate must additionally construct two separate `-I` roots containing the same canonical unit and require an ambiguity error; repeat the case with two paths to the same canonical file and require deduplication. It must also verify that `std.*` resolves only from the built-in std root, ordinary user units never do, and that changing a private non-generic implementation preserves the dependent interface-cache hit. Those checks need temporary roots/cache inspection and deliberately remain procedural rather than encoding host paths in fixtures. diff --git a/tools/tests/test_host_syntax.py b/tools/tests/test_host_syntax.py index 8fb4972..69a3e2b 100644 --- a/tools/tests/test_host_syntax.py +++ b/tools/tests/test_host_syntax.py @@ -17,13 +17,14 @@ What it still cannot see is the DOS environment itself -- memory limits, the command line length, the filesystem. The DOS build and the milestone suite remain the gate. -Skipped when the toolchain has not been downloaded, so the suite runs anywhere. +It uses the pinned toolchain only. There is no environment override and no +skip: a system-wide Watcom is a different version reporting different things, +and a gate that quietly skips is not a gate. """ from __future__ import annotations import os import re -import shutil import subprocess from pathlib import Path @@ -37,23 +38,20 @@ SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", "check", "lower", "emit_c", "driver") -def _watcom() -> Path | None: - """Prefer the toolchain the runner pins over anything installed system-wide.""" - root = os.environ.get("WATCOM") - candidates = [ROOT / ".dosboxx" / "watcom"] - if root: - candidates.append(Path(root)) - for base in candidates: - if (base / "binnt" / "wcl.exe").is_file(): - return base - return None - - @pytest.fixture(scope="session") def watcom() -> Path: - base = _watcom() - if base is None: - pytest.skip("run `ferro-dos setup` to download the pinned toolchain") + """The pinned toolchain, and nothing else. + + Deliberately no environment override and no skip. A system-wide Open Watcom + is a different version with different diagnostics, and a gate that skips is + a gate that is not running -- which is the failure mode this file exists to + close. dosboxx.py fails the same way when the toolchain is missing. + """ + base = ROOT / ".dosboxx" / "watcom" + if not (base / "binnt" / "wcl.exe").is_file(): + raise AssertionError( + f"the pinned Open Watcom is not at {base}; " + "run `uv run ferro-dos setup --accept-watcom-license`") return base From 2696dd2abc79f2b1d98170d7c52c0b1f8cce8ca0 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 02:42:12 +0900 Subject: [PATCH 110/184] docs: reduce SPEC.md to a language-only specification MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Abandon the milestone-driven organisation of SPEC.md. The document now contains only design philosophy (§1) and the language specification proper (lexical structure, types, ownership/borrow rules, grammar, semantics, module/unit semantics, and the minimal stdlib surface the language itself depends on). Removed: - All milestone content (M1-M12: descriptions, completion criteria, ordering) and the roadmap/schedule framing. - Compiler implementation directives: bootstrap strategy, pipeline, directory layout, C emission rules, own.c algorithm (§11 in full). - Test/fixture plans and pass/fail/run16/boot fixture listings (§12 in full). - Build-driver/tooling detail that isn't part of the language itself: import-root search and ambiguity resolution, .fei cache/hash format, the full CLI flag reference table, C-backend evaluation-order lowering notes, and the generic-instance C emission ordering. - SPEC.AUDIT.md entirely (git rm) — the accumulated change log for the old milestone-driven spec no longer applies. Kept and reorganised: §2-§9 (targets, lexical structure, type system, ownership/borrow rules R1-R11, grammar, statement/expression semantics, unit/import/visibility semantics, generics) are otherwise unchanged in wording. §10 (stdlib) is now a short placeholder noting the stdlib spec is pending, while retaining the minimal surface the language rules and builtins directly reference (core.Error, str alias methods, mem.replace/create/destroy/alloc_slice, io.Writer/Reader, fmt.fmt_*, sys.exit/on_exit). §13 (excluded features) is renumbered to §11 and kept as-is since it documents language-design decisions, not implementation. AGENTS.md's document map is updated to drop the SPEC.AUDIT.md row and reflect that SPEC.md is now language-only. Implementation and stdlib specs are intended to be written as separate documents going forward. --- AGENTS.md | 3 +- SPEC.AUDIT.md | 575 -------------------------------------------------- SPEC.md | 410 ++++------------------------------- 3 files changed, 38 insertions(+), 950 deletions(-) delete mode 100644 SPEC.AUDIT.md diff --git a/AGENTS.md b/AGENTS.md index ad78332..9f7b7af 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,8 +7,7 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 | 파일 | 역할 | |---|---| -| `SPEC.md` | 언어 명세 + 구현 지시서. 유일한 규범 문서 | -| `SPEC.AUDIT.md` | 명세 변경의 문제·결정·근거·구현 영향 누적 로그 | +| `SPEC.md` | 언어 명세. 유일한 규범 문서. 구현 지시서와 표준 라이브러리 명세는 별도 문서 | | `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | 개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다. diff --git a/SPEC.AUDIT.md b/SPEC.AUDIT.md deleted file mode 100644 index 2cf1441..0000000 --- a/SPEC.AUDIT.md +++ /dev/null @@ -1,575 +0,0 @@ -# Ferro specification audit log - -`SPEC.md`를 항상 최신 규범 문서로 유지하고, 최초 `AUDIT.md` 반영 이후 설계 판단으로 -바뀐 사항은 이 파일에 누적한다. - -## 2026-08-16 — v0.1.3 - -### `char`와 `u8` 사이의 변환 - -- 문제: §4.1은 `char`를 `u8`과 별개 타입으로 규정하고 암묵 변환을 금지하지만, - §6.4의 줄 수 계산 예제는 `[]u8`에서 얻은 값을 문자 리터럴과 직접 비교했다. -- 결정: 별개 타입과 암묵 변환 금지 원칙을 유지한다. 저장, 대입, 비교 모두 명시적인 - `as`가 필요하며 문자 리터럴도 문맥에 따라 자동으로 `u8`이 되지 않는다. -- 명세 반영: 예제의 비교를 `c.^ == '\n' as u8`로 수정하고 §4.1에 규칙을 명시했다. -- 구현 영향: 타입 검사기는 `char`를 독립 기본 타입으로 취급해야 하고, C 방출 시 같은 - 크기의 정수 표현을 사용할 수 있어도 Ferro 단계에서는 `char`/`u8` 혼용을 거부해야 한다. - -## 2026-08-16 — v0.1.4 - -### `match` scrutinee와 구조체 초기화의 중괄호 모호성 - -- 문제: `match expr { arms }`와 `Type{ fields }`가 모두 식별자 뒤에 `{`를 사용하므로 - `match value { ... }`의 arm 블록을 구조체 초기화로 잘못 소비할 수 있었다. -- 결정: `match` scrutinee 바로 뒤의 `{`는 항상 arm 블록으로 해석한다. 구조체 초기화식 - 자체를 scrutinee로 쓸 때는 `match (Type{ ... }) { ... }`처럼 괄호가 필수다. -- 구현 영향: match 문맥의 식 파서는 최상위 `{` 앞에서 scrutinee 파싱을 멈춰야 하며, - 괄호 안에서는 일반 구조체 초기화 규칙을 그대로 적용한다. 오류 복구는 모든 반복에서 - 적어도 한 토큰을 소비해 같은 진단을 무한 반복하지 않아야 한다. - -## 2026-08-16 — v0.1.5 - -### 제어 흐름 헤더와 구조체 초기화의 중괄호 모호성 일반화 - -- 문제: v0.1.4의 모호성은 `match`뿐 아니라 `if flag {}`, `while flag {}` 및 - `for x in values {}`처럼 식 직후 본문이 시작되는 모든 제어 흐름에 동일하게 발생한다. -- 결정: `if`, `while`, `for`, `match`, `comptime if` 헤더 바로 뒤의 최상위 `{`는 항상 - 제어 흐름 블록을 시작한다. 헤더 최상위에 구조체 초기화식을 쓰려면 괄호가 필수다. -- 구현 영향: 제어 흐름 헤더의 최상위 식 파싱에서 구조체 초기화를 금지하되 괄호 안에서는 - 일반 식 파싱 상태를 복원한다. 단순 식별자 조건과 배열·슬라이스 반복은 본문 `{` 앞에서 - 정상적으로 종료되어야 한다. - -## 2026-08-16 — v0.1.6 - -M6(borrow checker) 착수 전에 확정해야 하는 소유권·참조 규칙 결정과, M5까지 누적된 -문법·일관성 결함 정리를 함께 반영했다. - -### R8 — 파생 반환 규칙으로 전면 교체 - -- 문제: R4가 `[]T`를 함수 반환 타입에서 금지하고 기존 R8의 예외는 `&T`/`&mut T`만 - 다뤘다. 그 결과 §10이 요구하는 `str.trim`, `str.split_at`, `str.find`를 표현할 - 방법이 없었다. 기존 R8의 "결과를 지역 변수에 바인딩 불가" 제약도 `trim` 계열을 - 무의미하게 만들었다. -- 결정: R8을 파생 반환 규칙으로 교체한다. (a) 참조성 파라미터가 정확히 하나이고 - 반환값이 그것에서 파생될 때, (b) 문자열 리터럴이나 `static`에서 파생될 때 - `&T`/`&mut T`/`[]T`/`str`을 반환할 수 있다. 호출 지점에서 (a)의 결과는 그 인자를 - 대여한 것으로 취급하며, 바인딩 금지 제약은 삭제한다. -- 근거: 표기 없는 lifetime elision이며 정의와 호출 양쪽 모두 함수 하나만 보고 - 검증되므로 §1.2를 깨지 않는다. 바인딩 금지는 대여 추적을 피하려던 제약인데, - own.c가 R6를 위해 같은 상태 기계를 이미 돌리므로 추가 비용이 거의 없다. -- 구현 영향: own.c는 호출 결과에 "인자로부터의 대여" 상태를 전파해야 한다. - check.c는 시그니처만 보고 참조성 파라미터 개수와 가변성 관계를 검증한다. - 참조성 파라미터가 둘 이상이면 참조성 반환을 거부한다. - -### R6 — 대여 구간을 마지막 사용 지점까지로 축소 - -- 문제: 대여가 참조 변수의 스코프 끝까지 유지되고 블록 표현식도 없어서 - `let r = &mut x; r.^ = 1; x += 1;`이 에러였다. 회피 수단은 명시적 `{ }`뿐이며, - M11에서 컴파일러 B를 이 언어로 작성할 때 마찰이 누적된다. -- 결정: 대여 구간을 참조 변수의 마지막 사용 지점까지로 한다. 조건부 흐름에서는 - 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조는 문장 끝까지로 - 유지한다. -- 근거: 함수 지역 liveness 분석이므로 §1.2를 위반하지 않는다. -- 시점: M6 착수 전에 결정해야 한다. 나중에 좁히면 진단 메시지와 `fail/` 기대값을 - 전부 다시 써야 한다. -- 구현 영향: own.c에 역방향 liveness 스캔 한 번을 추가한다. - -### R10 — 전역에 대한 대여 금지 - -- 문제: R5가 참조 대상으로 전역을 허용하므로 `fn f(r: &mut i32) { G = 5; }`를 - `f(&mut G)`로 호출하면 R6의 배타성이 호출 경계에서 깨진다. 호출자는 `&mut G`가 - 배타적이라고 보고, 피호출자는 자기 파라미터가 `G`를 가리키는지 알 수 없다. - 함수 단위 지역 검사로는 원리적으로 검출 불가능하다. -- 결정: `static`(불변)만 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 - 대여 불가이며 직접 읽기/쓰기만 허용한다. `shared var`는 `critical` 안의 직접 - 접근만 허용한다. 전역을 참조로 넘겨야 하면 지역 변수로 복사한다. -- 구현 영향: R5의 대여 대상에서 가변 전역을 제외한다. §11.4에 방출 단계가 - aliasing을 가정하지 않는다는 규정을 추가했다. 이 규칙이 없으면 `&mut T`에 - `restrict`를 붙이거나 M13/M14 네이티브 백엔드에서 noalias를 가정하는 순간 - 불건전해진다. - -### `error.Name` 코드 부여 시점 - -- 문제: 코드를 "최종 링크용 생성 헤더의 심볼"로 참조하도록 규정했는데, 그러면 C에서 - 상수식이 아니므로 §11.4의 `switch` 방출을 쓸 수 없고 에러 `match`가 if-else - 체인으로 떨어진다. -- 검토 후 기각한 대안: 이름 문자열의 u16 해시. 유닛별 독립 계산과 캐시 유지라는 - 장점이 있으나 생일 문제로 이름 약 300개에서 충돌 확률이 50%에 달해 컴파일러 B의 - 에러 이름 규모를 감당하지 못한다. -- 결정: 정렬 기반 번호 부여는 유지하되, 부여 시점을 링크가 아니라 **드라이버의 emit - 이전 단계**로 옮긴다. 코드는 방출 C에서 컴파일타임 정수 상수가 된다. -- 대가: 이름 집합이 바뀌면 방출 `.c`와 오브젝트 캐시가 전부 무효화된다. `.fei`는 - 이름만 기록하므로 무효화되지 않는다. 유닛 단위 `--emit-c`는 `--error-table`로 - 확정 표를 받아야 한다. -- 구현 영향: driver.c가 전체 `.fei`에서 이름을 수집해 코드를 확정한 뒤 emit을 - 시작한다. - -### `str`을 `[]u8`과 별개 타입으로 - -- 문제: §4.2는 `str`을 "`[]u8` 불변 별칭"이라 하고 §4.7은 별칭을 완전 동일 취급이라 - 규정했다. 완전 동일이면 `str`을 통해 쓸 수 있는데, 문자열 리터럴은 읽기 전용 - 저장 영역에 놓이므로 안전성 구멍이다. -- 결정: `str`을 별개의 내장 타입으로 한다. `[]u8` → `str`은 `as str`로 변환 가능 - (가변성 약화이므로 안전), 역방향은 금지. `str` 원소 쓰기는 컴파일 에러. -- 구현 영향: §11.4에 `fe_str`(`const uint8_t*`) 방출 행을 추가했다. types.c는 - `str`을 `[]u8`과 다른 인터닝 엔트리로 다뤄야 한다. - -### `catch` 블록은 값을 만들지 않는다 - -- 문제: §4.6은 catch 블록이 값을 만들 수 있다고 했으나 문법에 블록 표현식이 없다. -- 결정: 블록 표현식을 도입하는 대신 catch 블록에서 값 생성을 금지한다. 블록은 - `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝난다. 값이 필요하면 - 짧은 형태 `expr catch <식>`을 쓴다. -- 근거: §1.5. 블록 표현식은 §13에 `편의` 등급으로 등재했다. - -### 문법 결함 정리 (§6.1) - -- `struct_decl`이 `field* fn_decl*`이라 §4.3 예제의 `pub fn new`가 문법 위반이었고 - §8이 요구하는 필드별 `pub`도 표현 불가였다. `member := ['pub'] (field | fn_decl)` - 으로 교체하면서 필드와 메서드의 순서 강제도 함께 풀었다. -- enum 배리언트 필드가 struct `field`를 재사용해 `pub`을 받을 수 있었다. `vfield`로 - 분리했다. -- `catch`/`orelse`는 §6.2 우선순위 표에 이름만 있고 프로덕션이 없었다. 추가했다. -- `error_decl`이 빈 에러 집합을 허용하고 마지막 쉼표를 강제했다. 최소 1개 + 선택적 - 후행 쉼표로 수정했다. -- §6.2에 `expr` 프로덕션이 없어 `try`, `as`, `@builtin`, 구조체 초기화가 EBNF - 어디에도 나오지 않았다. 우선순위 표가 표현식 문법의 규범임을 명시했다. -- `Some`/`None`이 패턴에 하드코딩돼 있으나 §3 예약어가 아니었다. 패턴 위치 전용 - 문맥 키워드임을 §4.5에 명시했다. - -### 일관성과 문서 정합 - -- §4.4의 `u16` 태그 승격 규정이 §11.4 방출 표의 `uint8_t` 고정과 어긋났다. 표를 - 수정했다. -- CLI 플래그가 §2, §7.4, §8, R3에 흩어져 있었다. §8.1로 통합했다. -- §11.3의 목표 디렉터리에 `own.c`/`lower.c`/`resolve.c`/`generic.c`/`rt/`가 있으나 - 실제로는 없다. 목표 구조는 유지하고, M5까지 `check.c`/`emit_c.c` 통합 상태이며 - M6 착수 시 `own.c/h`를 분리한다는 단서를 달았다. -- §12의 테스트 구조가 실제 `tests/m/`와 달랐다. 두 축을 모두 인정하도록 했다. - R8 변경에 따라 `fail/` 목록의 "참조 반환 바인딩" 항목을 교체하고 R10 전역 대여, - `str` 변환 케이스를 추가했으며 R6·R8 `pass/` 케이스를 신설했다. - -### 삭제 - -- `HANDOFF.md`를 제거했다. 여기에만 있던 빌드 함정(컴파일러 A는 16비트 large model, - 링크는 `*.obj`, M4 Watcom 테스트는 `-wx -wcd=202`, 8.3 파일명 제약, `D:`에서 빌드 - 금지)은 별도 문서로 옮겨야 한다. - -## 2026-08-16 — v0.1.7 - -외부 전면 audit에서 발견된 안전성 모순과 M6~M10 구현 전 미결정 사항을 통합했다. -이 절의 결정은 v0.1.6의 `str` nominal 타입, 함수 포인터 Writer, R8 단일 파라미터 -규칙 및 own.c 스코프 끝 해제 결정을 명시적으로 대체한다. - -### 슬라이스·문자열·소유 버퍼 - -- 문제: R4는 일반 `^T`의 대상에 `[]T`를 금지하면서 `^[]T`, `List.items`, - `mem.alloc_slice`를 요구했다. 또한 하나뿐인 `[]T`가 읽기/쓰기를 모두 나타내어 - R6의 공유·배타 대여를 표현할 수 없었다. -- 결정: `[]T`는 공유·읽기 전용, `[]mut T`는 배타·쓰기 가능 slice다. var place만 - mutable slice를 만들 수 있다. 호출 인자 위치의 `[]mut T → []T`, `&mut T → &T`는 - 원래 Exclusive 상태를 유지하는 암묵 재대여로 한정한다. -- 결정: `^[]T`는 일반 포인터 합성이 아닌 `(ptr,len)` 독립 소유 타입이다. - `^[]T`/`?^[]T`만 R4의 대상 제한에서 예외이고 `*[]T`/`*[]mut T`는 금지한다. -- 재검토: v0.1.6은 문자열 리터럴의 불변성을 위해 `str`을 nominal 타입으로 만들었지만, - `[]T` 자체가 불변이 되면서 근거가 사라졌다. `str`을 미리 선언된 `[]u8` 완전 동일 - alias로 내렸다. UTF-8 검증은 없으며 별도 cast·C 표현·쓰기 금지 규칙이 필요 없다. -- 결과: `str`도 R4를 그대로 적용받아 field/element에 저장할 수 없다. 문자열을 - 소유하려면 `String{ bytes: ^[]u8 }`를 쓰며 `as_str()`은 파생 shared slice를 반환한다. - -### 안전한 Writer/Reader와 포매팅 분리 - -- 문제: 안전한 `File.writer() -> Writer`가 대여 대상을 `*void`에 숨겨 반환하여 - `make() -> Writer`만으로 safe-code dangling을 만들 수 있었다. 문서의 "사용자 책임"은 - §1의 memory-safety 보장과 충돌하며 Reader도 동일하게 불건전했다. -- 원칙: 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 - 반환할 수 없다. R9 내부에서 unsafe 변환을 한 번 감쌌다는 사실은 safe API를 - 건전하게 만들지 않는다. -- 결정: v0.1.2 Writer/Reader는 함수 포인터 struct 대신 정수 payload만 가진 Copy handle - enum이다. `Writer{Stdout,Stderr,File(u16),Null}`, `Reader{Stdin,File(u16)}`와 - `io.write(Writer, []u8)`, `io.read(Reader, []mut u8)`를 쓴다. fd 재사용은 논리적 I/O - 오류일 수 있으나 memory dangling은 아니다. buffer Writer는 두지 않는다. -- 결정: fmt는 sink를 모른다. `fmt.fmt_int_i32(tmp: []mut u8, v) -> str`처럼 임시 - buffer에 쓰고 R8(a) 파생 slice를 반환하는 순수 함수 한 벌만 둔다. `@print`/`@fprint`는 - 결과를 `io.write`, `@sprint`는 `mem.copy`로 이어 붙인다. v0.2의 `dyn Writer` 전환은 - 안전성 수정이 아닌 기능 확장이다. - -### 소유권·R4·R8 - -- `str`/`[]T`/조건부·에러 union/배열의 재귀 Copy 규칙을 완성하고 `[]mut T`와 - `^[]T`는 non-Copy로 정했다. -- field/index/optional projection에서 non-Copy 값을 부분 이동하는 것을 금지했다. - own.c는 변수 단위 상태를 유지하며 `mem.replace(&mut place, replacement)`만 추출을 - 허용한다. `.?`/field/index는 값을 즉시 꺼내는 연산이 아니라 place projection이다. -- mutable borrow·slice와 `&mut Self` 호출은 var place에서만 허용한다. consuming - `self: Self`는 메서드 내부에서 invalid sentinel을 남길 수 있는 local owner다. -- R8 메서드는 파생 원본을 self로 고정한다. 추가 참조 인자는 받을 수 있지만 반환이 - 그 인자에서 파생될 수 없다. 자유 함수만 참조성 파라미터 정확히 하나를 요구한다. - `?&T`, `?&mut T`, `?[]T`, `?[]mut T` 반환을 포함한다. -- own.c의 오래된 "스코프 끝 해제"와 "R8 결과 바인딩 거부"를 삭제했다. 역방향 - liveness pass로 마지막 사용을 계산하고 defer 사용은 스코프 끝까지 연장한다. - -### drop, File, heap 초기화 - -- `File.close(=drop)` 모순을 제거했다. `close(self: Self) -> !void`는 소비하는 일반 - 메서드이며 내부 handle을 invalid로 만든 후 오류를 반환한다. 자동 drop은 열린 handle만 - 조용히 닫는다. `drop` 직접 호출 금지는 유지한다. -- `mem.create(T) -> !^T`는 초기화되지 않은 안전 힙을 반환하므로 삭제했다. - `mem.create(value: T) -> !^T`로 바꾸고 T는 값에서 추론한다. - -### error와 결정적 build - -- `try`는 operand와 현재 함수의 nominal error 타입이 같을 때만 허용한다. 다른 타입은 - catch에서 명시 매핑한다. `catch`는 error-return 함수 밖에서도 허용하며 void 결과의 - handler block은 정상 fallthrough할 수 있다. -- 정렬 기반 error code 표는 결정성과 fixpoint를 위해 유지한다. build-directory 이력에 - 의존하는 append-only 표는 기각했다. -- 드라이버는 단일 `fe_errors.h`에 정렬된 `#define`을 생성한다. 이름 집합 변경 시 유닛 - C를 재방출하지 않고 header 의존 object만 다시 컴파일한다. switch 상수 요건도 유지한다. - -### 제네릭·증분 build - -- 제네릭 이름 해석은 사용 유닛이 아니라 정의 유닛 scope에서 한다. `.fei`는 본문 token과 - generic 전용 private signature를 함께 기록한다. -- driver가 전체 인스턴스 요청을 합쳐 단일 `fe_generics.c`에 중복 없이 방출한다. - 사용 유닛별 external 중복 심볼과 static 코드 복제를 모두 피한다. -- comptime type 비교와 최소 introspection `@is_int`, `@is_ptr`를 추가했다. -- `.fei` cache key에 source hash뿐 아니라 dependency `.fei` hash를 포함한다. - -### interrupt/shared와 panic - -- shared C 방출을 `volatile`로 정하고 critical 진입·이탈에 compiler barrier를 둔다. - bits16의 한 명령 크기 8/16비트 atomic load/store는 interrupt 경계에서 원자적이므로 - 자동 critical 없이 volatile 한 명령만 방출한다. far pointer와 RMW는 explicit critical이다. -- `interrupt_safe`에서 critical, port/volatile builtin, asm과 필요한 unsafe를 허용한다. - 금지 목록은 heap, DOS/DPMI, blocking I/O, FPU, non-interrupt-safe 호출로 한정했다. -- panic은 일반 defer unwind를 하지 않지만 interrupt vector 복원용 고정 크기 - `sys.on_exit` callback을 실행한다. bits32 interrupt/shared/critical은 v0.2로 명시했다. - -### 문법·표기 정리 - -- generic struct/enum parameter, declaration-level comptime if, for 전용 range, - bool/char pattern, `[]mut T`를 EBNF에 추가했다. -- 정의되지 않은 단항 `^`를 삭제했다. field/index/slice/method의 `&`/`&mut`/`^` - projection과 raw/optional의 비자동 역참조를 명문화했다. -- method가 function-pointer field보다 우선하며 field 호출은 `(x.f)(...)`로 고정했다. -- `@seg_ptr(T, seg, off)`로 타입 인자를 명시하고 type-valued const alias를 허용했다. -- 단항 직후 cast는 `(-x) as T` 또는 `-(x as T)`처럼 괄호를 강제한다. - -### 구현 및 milestone 영향 - -- M3: shared/mutable slice와 str alias를 재검증한다. -- M4: 함수 포인터 Writer를 handle enum + 순수 fmt 함수로 교체한다. -- M5: `^[]T`, consuming close, projection 부분 이동, 초기화된 create를 반영한다. -- M6: 역방향 liveness와 self-source R8을 구현한다. -- M7: try nominal error 일치와 일반 catch를 구현한다. -- M8/M9: `fe_errors.h`, dependency hash, `fe_generics.c`를 구현한다. -- M10: volatile/barrier, interrupt-safe 허용 목록, on_exit 복원을 검증한다. - -## 2026-08-16 — v0.1.8 - -M6~M9 구현 전에 함수-local 소유권 분석, optional/error 의미, 계층형 unit/import, -`.fei` cache와 제네릭 모노모피제이션을 동결했다. compiler A/B가 같은 작은 상태 기계를 -구현하고 DOS와 host에서 같은 source graph를 선택하며 M12 fixpoint에서 byte-identical -출력을 만들 수 있는지가 공통 판단 기준이다. - -### Deterministic expression evaluation order - -- 문제: C는 일반 호출 인자와 많은 operand의 평가 순서를 보장하지 않는다. Ferro가 이를 - 그대로 상속하면 side effect뿐 아니라 move, borrow, `try`, defer/drop cleanup 결과가 C - compiler와 최적화에 따라 달라진다. -- 결정: Ferro 일반 표현식은 left-to-right다. 호출은 callee 먼저, 이어서 source 순서의 - 인자, 이항식은 왼쪽 operand 먼저다. `and`/`or`, `orelse`, `catch`는 필요한 우변만 - 평가하는 lazy 연산이다. -- 근거: source만으로 동작과 cleanup 순서를 예측할 수 있고 compiler A/B의 lower 결과가 - 동일해진다. 함수-local 분석 원칙도 그대로 유지한다. -- 기각한 대안: target C의 평가 순서에 맡기기. host compiler와 build option에 따라 의미가 - 변해 M12 결정성을 깨므로 기각했다. -- 구현 영향: lower는 C에서 순서가 보장되지 않는 식을 ordered temporary statement로 - 분해하고 own.c도 같은 순서로 place effect를 처리한다. - -### M6 root-granularity borrow tracking - -- 문제: field/index별 독립 대여를 허용하려면 projection overlap, 동적 index 동등성, - union/alias까지 다루는 별도 alias analysis가 필요하다. -- 결정: v0.1 대여 상태는 root local/parameter 단위다. `&mut p.a`는 `p` 전체를 잠그고 - `xs[0]`과 `xs[1]`도 같은 root의 충돌 대여다. projection은 root를 찾는 데만 쓴다. -- 근거: R1~R8을 함수 하나의 작은 상태 기계로 검사할 수 있어 compiler A와 M11의 B가 - 단순해진다. 보수적 거부일 뿐 memory safety나 표현 결정성은 약화하지 않는다. -- 기각한 대안: field-sensitive/index-sensitive borrow checking. 편의는 늘지만 compiler A의 - 구현량과 진단 상태가 크게 증가하고 동적 index에는 결국 보수성이 남아 기각했다. -- 구현 영향: own.c의 borrow key는 projection이 아니라 root symbol이다. M6에는 disjoint - field/index도 충돌하는 pass/fail 경계를 고정한다. - -### M6 reborrow/coercion 제한 - -- 문제: `&mut → &`와 `[]mut → []`를 일반 암묵 변환으로 허용하면 새 shared borrow의 - 수명과 원래 exclusive borrow의 재활성화를 결정하는 숨은 coercion/lifetime 시스템이 - 필요하다. -- 결정: 암묵 약화는 호출 인자 위치의 호출 기간 read-only reborrow만 허용한다. 원래 - exclusive borrow는 원래 last-use까지 유지하며 일반 `let`/대입의 암묵 약화는 에러다. -- 근거: API 호출 편의는 확보하면서 수명 annotation 없이 함수-local R6 분석을 유지한다. -- 기각한 대안: arbitrary implicit reborrow/coercion과 `let s: &T = m` 허용. 대여 종료 - 시점이 숨고 compiler A/B가 별도 coercion graph를 가져야 하므로 기각했다. -- 구현 영향: check/own은 call argument에만 임시 shared view 전이를 만들고 assignment - conversion table에는 추가하지 않는다. - -### R8 provenance lattice - -- 문제: 여러 return path의 static/parameter 파생 결과, optional null 경로와 method의 - 추가 참조 인자를 합칠 명시 규칙이 없으면 caller borrow가 구현 순서에 따라 달라진다. -- 결정: provenance를 `Static`과 `Param(N)`으로 정규화한다. method는 `Param(self)`만, - 자유 함수는 유일한 참조성 parameter만 허용한다. `Static + Param(N)`은 `Param(N)`, - 서로 다른 `Param`의 합류는 에러다. null 경로는 caller borrow가 없는 경로다. -- 근거: provenance가 작은 lattice라 함수 본문만 보고 계산하고 시그니처로 전달할 수 있다. - lifetime annotation이나 interprocedural inference가 필요 없다. -- 기각한 대안: arbitrary parameter union provenance 또는 lifetime parameter. caller에서 - 숨은 alias set/전역 분석이 필요해 Ferro 철학과 맞지 않는다. -- 구현 영향: own.c가 return CFG에서 lattice를 합치고 lowered signature와 `.fei`가 - provenance metadata를 보존한다. - -### M6 branch merge와 loop fixed point - -- 문제: `Owned/Moved`, 초기화 여부와 live borrow가 branch/backedge에서 만날 때 단순히 - 한쪽 상태를 고르면 use-after-move를 놓치거나 안전한 borrow를 너무 일찍 푼다. -- 결정: `Owned + Moved → MaybeMoved`, 경로별 초기화 차이는 `MaybeUninit` 동등 상태로 - 합친다. live borrow는 합집합을 보수적으로 유지하고 incompatible borrow는 약화하지 - 않는다. loop은 진입/종료 상태를 합쳐 두 번째 pass를 돌리고 안정되지 않으면 에러다. -- 근거: 유한한 함수-local lattice와 기존 2-pass만으로 모든 iteration을 보수적으로 - 근사한다. 첫 iteration만 검사하는 불건전성을 피한다. -- 기각한 대안: 첫 pass만 검사, 또는 merge에서 borrow를 `Owned`로 되돌리기. loop-carried - alias와 조건부 move를 놓치므로 기각했다. -- 구현 영향: own.c는 branch exit 전에 last-use를 반영하고 merge table/bit state를 - 구현한다. runtime drop에는 `MaybeMoved` live flag가 필요하다. - -### M7 contextual null/error-union construction - -- 문제: `null`에 독립 타입을 주거나 error union을 일반 implicit conversion으로 다루면 - 타입 추론·overload 후보가 늘고 nominal error 경계가 흐려진다. -- 결정: `null`은 expected optional/pointer-like type이 유일할 때만 구성된다. expected - `E!T` 위치에서는 T가 success, E가 failure를 구성하며 return도 같다. E1/E2 및 - nominal error/`core.Error` 자동 변환은 없다. -- 근거: contextual expected type 한 개만 보면 되어 compiler A/B의 local type checker가 - 결정적이고 nominal error 안전성도 유지된다. -- 기각한 대안: polymorphic null, 일반 union injection conversion, error widening. 숨은 - conversion 및 overload resolution을 요구하므로 기각했다. -- 구현 영향: check는 expected-type 전달 위치에서만 null/error construction을 허용하고 - 문맥 없는 `let p = null`을 진단한다. - -### M7 error declaration uniqueness - -- 문제: code 0 외에도 한 nominal error 선언 안의 중복 member 이름이나 숫자 code는 - match/format 결과를 모호하게 만든다. -- 결정: code 0, 중복 이름, 중복 숫자 code를 모두 compile error로 한다. 서로 다른 nominal - error 선언끼리는 같은 숫자를 사용할 수 있지만 타입은 계속 다르다. -- 근거: 선언 하나의 symbol/code table만 검사하면 되고 runtime representation은 바뀌지 - 않는다. -- 구현 영향: error declaration check가 두 deterministic set을 만들고 중복 위치를 note로 - 표시한다. - -### M7 lazy recovery operators / non-Copy extraction - -- 문제: `orelse`/`catch` RHS를 eager 평가하면 불필요한 side effect와 move가 생긴다. - 또한 `Some(x)` pattern이나 projection이 non-Copy payload를 암묵 이동하면 R7과 - 조건부 drop이 불명확해진다. -- 결정: recovery RHS/handler는 failure 경로에서만 평가한다. optional pattern은 place의 - borrow/view이고 Copy만 복사한다. non-Copy owned payload 추출은 `mem.replace`로만 하며 - temporary optional 자동 추출 예외도 두지 않는다. -- 근거: 평가와 소유권 효과가 같은 CFG 경로를 따르고 기존 projection/R7 상태 기계를 - 재사용한다. -- 기각한 대안: pattern별 destructive move와 temporary 특례. hidden move와 추가 drop - 상태를 만들고 source에서 비용이 보이지 않아 기각했다. -- 구현 영향: lower는 lazy branch를 만들고 own은 실행 경로별 effect를 합친다. pattern - binding은 place mutability에 따른 shared/mutable borrow다. - -### Hierarchical dotted unit namespace - -- 문제: 단일 `unit foo` namespace는 외부 source library가 늘 때 `util`, `types`, `parse` - 같은 이름 충돌을 피할 수 없다. -- 결정: `unit_path := ident ('.' ident)*`, `import unit_path [as ident]`의 계층형 canonical - 이름을 도입한다. import binding은 마지막 segment이고 항상 `binding.member`로 접근한다. -- 근거: Go와 비슷한 단순 unit 전체 import를 유지하면서 namespace 충돌만 해결한다. - resolver에는 relative scope walk나 symbol import가 필요 없다. -- 기각한 대안: relative/glob/selective imports, re-export, friend/package-private visibility. - 이름 해석과 캐시 의존 graph가 복잡해져 v0.1에서 제외했다. -- 구현 영향: lexer keyword 추가는 없고 parser/resolve/diagnostic이 canonical dotted path와 - optional alias를 보존한다. - -### DOS-safe unit naming - -- 문제: host의 case sensitivity와 FAT 8.3 규칙이 다르면 같은 source tree가 다른 unit을 - 찾거나 긴 이름 전송 시 변형될 수 있다. -- 결정: unit segment는 lowercase ASCII, 첫 글자 letter, 이후 letter/digit/underscore, - 최대 8자로 제한한다. dotted path는 root 아래 `segment/.../last.fe`와 정확히 대응하고 - 비교는 규범적 ASCII case-insensitive mapping을 쓴다. -- 근거: unit identity가 DOS와 host에서 같고 8.3 alias 생성에 기대지 않는다. -- 기각한 대안: 일반 Ferro identifier/임의 길이 허용 후 host별 normalization. case-fold와 - truncation 충돌이 platform-dependent라 기각했다. -- 구현 영향: entry path suffix로 project root를 계산하고 mismatch/case-variant duplicate를 - 진단한다. canonical identity는 항상 lowercase dotted form이다. - -### Deterministic import-root resolution / ambiguity rejection - -- 문제: project root, 여러 `-I`, std root를 first-match-wins로 검색하면 `-I` 순서나 host - directory 상태가 실제 선택 source를 바꾼다. -- 결정: 모든 candidate root를 조사하고 동일 unit에 서로 다른 canonical file이 둘 이상 - 있으면 ambiguous error와 path note를 낸다. 같은 실제 file alias만 dedup한다. -- 근거: build/order/platform과 무관한 source graph를 만들어 `.fei`, generated C와 M12 - fixpoint를 안정시킨다. -- 기각한 대안: first-match-wins. 편하지만 shadowing이 command-line order에 숨어 결정성을 - 깨므로 기각했다. -- 구현 영향: driver는 후보를 canonicalize·정렬한 뒤 identity를 비교하고 첫 성공에서 - 검색을 중단하지 않는다. - -### Reserved std namespace - -- 문제: flat `io`, `mem`, `fmt`, `sys`는 user library 이름과 충돌하고 compiler 내장 std - root를 일반 user root처럼 검색하면 같은 이름이 환경에 따라 shadow된다. -- 결정: `std` top-level을 compiler-reserved로 하고 `std.io`, `std.mem`, `std.fmt`, - `std.sys`를 canonical unit으로 쓴다. 마지막 segment binding 때문에 사용 표면은 - `io.write`, `mem.replace`로 유지한다. `str`은 import unit이 아니다. -- 근거: std lookup이 명시적이고 deterministic이며 향후 source package와 충돌하지 않는다. -- 구현 영향: M8에서 std source layout/unit 선언을 이동하고 builtin std root는 `std.*`에만 - 후보가 된다. - -### Source-only external libraries - -- 문제: v0.1에서 package manifest/solver나 stable binary ABI까지 정의하면 M8 범위를 넘어 - `.fei` encoding과 target C ABI를 영구 호환 계약으로 굳히게 된다. -- 결정: 외부 library는 source tree를 `-I` root로 제공한다. package manager/registry/version - solver/manifest 문법과 `.fei + .obj/.lib` binary-only 배포 ABI는 지원하지 않는다. -- 근거: 언어 import 의미는 작게 유지하고 target/model별로 source에서 결정적으로 - 재컴파일할 수 있다. DOS 배포 도구도 단순하다. -- 기각한 대안: package manager를 언어 의미론에 결합, binary-only ABI. compiler A와 - M12 전에 해결할 필요가 없고 호환 부담이 커 기각했다. -- 구현 영향: `-I`는 source-only candidate root이며 미래 도구도 root 구성만 담당한다. - -### `.fei` interface/cache role과 deterministic schema - -- 문제: source hash, public interface hash와 compile cache key가 섞여 있었고 `.fei`의 - 최소 논리 정보·결정적 직렬화 조건이 없어 private 변경이 전체 rebuild를 유발하거나 - host path/timestamp가 fixpoint에 섞일 수 있었다. -- 결정: 세 hash 개념을 분리하고 `.fei`에 version/target/model, canonical unit, public - signatures/layout, anonymous errors, exported generic body/support closure, direct dependency - names/hashes를 기록한다. canonical key 순으로 직렬화하며 absolute path/time/build dir를 - 금지한다. -- 근거: private non-generic 변경은 자기 unit만 재컴파일하고 interface가 같으면 dependent를 - 유지할 수 있다. 같은 graph+target의 `.fei`는 host/build order와 무관하게 byte-identical하다. -- 기각한 대안: source hash를 interface hash로 재사용, unordered serializer. 구현은 짧지만 - 불필요한 rebuild와 M12 비결정성을 만들어 기각했다. -- 구현 영향: `.fei` encoding은 자유지만 논리 schema와 sorted serialization을 만족하고 - driver cache가 dependency interface hash를 사용해야 한다. - -### Ferro visibility vs backend linkage - -- 문제: Ferro private를 무조건 C `static`으로 방출하면 통합 `fe_generics.c`의 exported - instance가 definition unit private helper를 호출할 수 없다. -- 결정: Ferro private는 resolver visibility일 뿐 C linkage와 동일하지 않다. generic - support에 필요한 private top-level symbol은 deterministic unit-mangled linkage와 internal - prototype을 가질 수 있다. -- 근거: source 접근 권한은 유지하면서 단일 통합 generic body 방출을 가능하게 한다. -- 기각한 대안: `C static == Ferro private`, 또는 private helper를 instance마다 복제. - 전자는 linkage 실패, 후자는 중복과 비결정적 출력 때문에 기각했다. -- 구현 영향: resolver는 여전히 cross-unit private 참조를 거부하고 backend/internal header만 - `.fei` support metadata를 통해 symbol을 연결한다. - -### M9 type-only generics - -- 문제: user comptime value generic까지 허용하면 값 canonicalization, mangling, expression - evaluator와 instance explosion 정책을 M9에서 함께 설계해야 한다. -- 결정: v0.1 user generic parameter는 `type`만 지원한다. `struct Box(T)`는 - `comptime T: type` shorthand이며 builtin comptime value와 구별한다. -- 근거: List/Map과 compiler B에 필요한 추상화를 충족하면서 instance key를 canonical type - list로 제한한다. trait/bound도 추가하지 않는다. -- 기각한 대안: integer/string/bool value generics. M11 필수 기능이 아니고 구현/결정성 - 부담이 커 v0.1 이후로 미룬다. -- 구현 영향: parser/check가 user generic parameter type을 제한하고 value generic fixture를 - 명시적으로 거부한다. - -### M9 no type inference - -- 문제: `id(3)`에서 T를 추론하려면 argument constraints, conversion 후보와 향후 overload - 규칙을 정의해야 하고 진단/instance 발견 순서도 복잡해진다. -- 결정: generic type argument는 `id(i32, 3)`처럼 항상 명시한다. compiler-known - `mem.create(value)` inference는 별도 intrinsic 규칙이다. -- 근거: call syntax만 보고 instance key가 결정되어 compiler A/B가 단순하고 deterministic하다. -- 기각한 대안: generic type inference. 편의보다 숨은 constraint solver 비용이 커 기각했다. -- 구현 영향: generic call arity/type argument check는 명시 목록만 검사하며 inference - fallback을 시도하지 않는다. - -### Definition-site resolution - -- 문제: generic body의 non-dependent 이름을 caller scope에서 다시 찾으면 caller의 import와 - shadowing에 따라 같은 generic이 다른 코드를 만든다. -- 결정: 이름은 definition unit에서 고정하고 type-dependent operation만 instantiation 때 - 검사한다. `comptime if`의 선택되지 않은 branch는 parse만 하고 semantic 처리하지 않는다. -- 근거: lexical 의미와 private support를 유지하고 caller/build order와 무관한 instance를 - 만든다. -- 기각한 대안: use-site lookup과 selected-out branch의 eager type check. 전자는 의미가 - 불안정하고 후자는 type-specific branch를 불가능하게 해 기각했다. -- 구현 영향: `.fei`가 body token과 definition-scope symbol/support identity를 전달하고 - generic.c가 그 환경에서 재검사한다. - -### Deterministic monomorphization - -- 문제: request 발견 순서대로 instance를 방출하면 unit traversal/hash iteration/parallel - build에 따라 `fe_generics.c`와 symbol 순서가 바뀐다. -- 결정: canonical definition unit + declaration identity + normalized canonical type args를 - key로 dedup하고 byte ordering으로 정렬한다. prototype 전부를 먼저, body 전부를 나중에 - 같은 canonical 순서로 방출한다. -- 근거: alias instance가 중복되지 않고 recursion/cross-instance call을 지원하며 M12에서 - byte-identical output을 만든다. -- 기각한 대안: first-request order 또는 pointer/insertion-order key. platform/build order에 - 의존해 기각했다. -- 구현 영향: driver/generic.c가 global request set을 정렬하고 alias를 underlying interned - identity로 normalize한다. - -### Generic private-support closure - -- 문제: exported generic이 private helper/type/const/private generic을 참조하면 signature만 - 담은 `.fei`로 다른 unit에서 안전하게 instantiate할 수 없다. -- 결정: `.fei`는 필요한 private support dependency의 transitive closure를 compiler-only - metadata로 제공한다. 이는 Ferro visibility를 public으로 바꾸지 않는다. -- 근거: definition-site semantics와 source private API를 동시에 지키며 `fe_generics.c`에서 - 정확한 backend symbol/layout을 사용할 수 있다. -- 기각한 대안: 모든 support를 source `pub`으로 강제하거나 generic body를 definition unit마다 - static 복제. API 누출 또는 중복/링크 문제 때문에 기각했다. -- 구현 영향: interface hash는 exported generic이 관찰하는 support 변화에 반응하고 - serializer는 closure를 canonical 순서로 기록한다. - -### Recursive instantiation semantics - -- 문제: 단순 재귀 호출이 같은 instance를 다시 요청할 때마다 depth를 올리면 정상 generic - recursion도 limit에 걸리고, 반대로 growing type chain을 dedup만으로 허용하면 무한 생성된다. -- 결정: pending/known 동일 key 재요청은 재사용하고 depth를 소비하지 않는다. 새로운 distinct - instance chain만 증가시키며 32 초과를 에러로 한다. -- 근거: ordinary recursion은 prototype-first 방출로 처리하고 실제 instance explosion만 - 유한한 local driver 상태로 차단한다. -- 구현 영향: generic.c는 pending/known set과 distinct chain stack을 구별하고 최초/현재 - instantiation 위치를 note로 출력한다. - -### Canonical C mangling and type identity - -- 문제: unit이 계층화되고 generic이 통합 방출되면 host path, pointer address 또는 insertion - order 기반 이름은 충돌하거나 run마다 달라질 수 있다. -- 결정: mangling은 canonical dotted unit + declaration + normalized canonical type args만 - 사용한다. nominal identity는 fully-qualified defining unit+name, alias는 underlying identity다. -- 근거: collision-free backend linkage와 M12 fixpoint를 동시에 보장한다. -- 구현 영향: 정확한 escaping 문자는 구현 세부지만 deterministic separator encoding과 - collision 검사가 필요하며 absolute path/address를 symbol에 포함할 수 없다. - -### `try` enforcement is blocked on M7 contextual construction - -- 문제: `SPEC.md`는 `try`를 에러 유니온 반환 함수 안에서만 허용하는데, `check.c`의 검사가 - `FE_N_EXPR_STMT`에만 걸려 있어 `var x = try e;`와 `x = try e;`를 통과시킨다. - `fec/tests/m5/runtime.fe`의 `run`과 `owned.fe`의 `main`이 이 구멍에 의존하고 있었다. -- 결정: 구멍은 M7과 함께 닫는다. M7 이전 master에서는 닫을 수 없다. (M7에서 해소됨) -- 근거: 검사를 `try` 표현식으로 옮기면 `run`이 에러 유니온을 반환해야 하는데, master는 - `-> !i32`에서 `return ;`도, `-> !void`에서 명시적 `return;`도 거부한다 - ("return type mismatch" / "void expression returned from value function"). 둘 다 - contextual success construction이 필요하고 그것은 M7 작업이다. `catch`와 `@trap`도 - master에는 없어서 우회로가 없다. 실측으로 세 경로를 모두 확인했다. -- 구현 영향: M7 병합 시 `check.c`의 검사를 `check_expr`의 `try` 분기로 옮기고 - `FE_N_EXPR_STMT`의 중복 검사를 제거한다. `runtime.fe`의 `run`은 그때 에러 유니온 - 반환으로 바꾸고 `runtime.c`의 `extern long fe_m5_runtime_run(long)`을 함께 고친다. - `owned.fe`의 `main`은 M7 없이도 합법인 `-> !void`로 먼저 고쳐 두었다. diff --git a/SPEC.md b/SPEC.md index 5941711..8eb5444 100644 --- a/SPEC.md +++ b/SPEC.md @@ -3,7 +3,8 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. 파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. -이 문서는 언어 명세 + 컴파일러 구현 지시서를 겸한다. 구현 중 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. +이 문서는 Ferro 언어 명세만 다룬다. 컴파일러 구현 지시서와 표준 라이브러리 상세 명세는 +별도 문서에서 다룬다. 명세 판단이 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. --- @@ -13,7 +14,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 640KB 머신에서 셀프호스팅을 가능하게 한다. 3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음. 4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능. -5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §13에 기록한다. +5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §11에 기록한다. 6. **기존 도구체인 재사용.** 링커, `.OBJ`/`.LIB`/`.EXE` 포맷, DPMI 익스텐더를 새로 만들지 않는다. --- @@ -28,10 +29,8 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 | 메모리 모델 | small, large | flat | | 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | -- CLI: `fec main.fe --target=bits16|bits32 [--model=small|large] [--strip-error-names]` - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. -- 표준 라이브러리는 코어 공용, `std.sys` 유닛만 타깃별 구현. --- @@ -172,7 +171,7 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 - `try`는 에러 유니온 반환 함수 안에서만 허용한다. 피연산자의 nominal error 타입은 현재 함수의 error 타입과 정확히 같아야 한다. 다르면 `catch`에서 명시적으로 매핑한다. - `catch`는 현재 함수의 반환 타입과 무관하게 어디서든 에러를 그 자리에서 처리할 수 있다. - `try e`: 에러면 현재 함수에서 즉시 반환한다. -- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §13 참조.) +- `e catch |x| { ... }`: 블록은 값을 만들 수 없다. 결과 타입이 `void`이면 정상적으로 끝까지 실행할 수 있고, 값 결과가 필요하면 `return`/`break`/`continue`로 탈출하거나 `@trap()`으로 끝나야 한다. 값이 필요하면 아래 짧은 형태를 쓴다. (언어에 블록 표현식을 도입하지 않기 위한 선택. §11 참조.) - `e catch default_value`: 짧은 형태. 우변은 식이며 그 값이 결과가 된다. - 짧은 `catch`의 우변과 block `catch`의 handler는 피연산자가 error일 때만 평가·실행한다. success이면 handler의 부수 효과·이동·대여가 발생하지 않는다. - 서로 다른 error 타입 간 자동 변환 없음. `!T`(기본 에러 집합 `core.Error`)로 통일하거나 명시 매핑. @@ -588,15 +587,13 @@ pub fn main() -> !void { - 이항 연산자는 왼쪽 operand를 먼저, 오른쪽 operand를 나중에 평가한다. - `and`와 `or`는 왼쪽 operand로 결과가 정해지면 오른쪽을 평가하지 않는다. `orelse`와 `catch`도 §4.5·§4.6에 따라 우변/handler가 필요한 경로에서만 평가한다. - 이 순서는 부수 효과뿐 아니라 move, borrow의 시작·마지막 사용, `try` 전파와 defer/drop cleanup 순서를 결정한다. -- C backend는 C 자체의 미지정 평가 순서에 의존할 수 없다. C에서 순서가 보장되지 않는 호출 인자, 일반 이항 operand 등의 부수 효과·이동·대여는 lower 단계에서 순서가 명시된 temporary statement로 분해한다. `and`/`or` 같은 C의 단락 규칙을 직접 사용하더라도 Ferro의 lazy 의미를 그대로 보존해야 한다. --- -## 8. 유닛과 빌드 +## 8. 유닛 파일 하나가 유닛 하나다. 유닛의 canonical identity는 fully-qualified dotted unit path이며 -모든 cross-unit 타입·선언·제네릭 identity, `.fei`, C mangling, cache key와 진단에서 같은 -이름을 사용한다. +모든 cross-unit 타입·선언·제네릭 identity에서 같은 이름을 사용한다. ```fe unit game.main; @@ -612,8 +609,12 @@ import는 항상 유닛 전체를 가져오며 member는 local unit binding으 `parse.read(...)`, `import std.io;` 뒤에는 `io.write(...)`를 쓴다. `as`가 있으면 그 alias가 binding이다. +`std` 최상위 namespace는 compiler-reserved이며 user unit은 선언할 수 없다. 표준 유닛은 +`import std.io;`, `import std.mem;`, `import std.fmt;`, `import std.sys;`처럼 가져온다. +`str`은 계속 built-in `[]u8` alias와 alias-method namespace이며 import unit이 아니다. + v0.1은 relative import(`.foo`, `..foo`), glob/selective import, `pub import` re-export, -package-private/friend visibility를 지원하지 않는다. +package-private/friend visibility, package manager를 지원하지 않는다. ### 8.1 unit 이름과 source path @@ -631,42 +632,7 @@ tinyjson.parse -> tinyjson/parse.fe game.world.map -> game/world/map.fe ``` -entry source의 선언이 `unit game.main;`이고 실제 파일이 -`C:/PROJECT/SRC/GAME/MAIN.FE`이면 file path 끝의 `game/main.fe` suffix를 ASCII -case-insensitive 방식으로 비교해 제거하고 project source root `C:/PROJECT/SRC`를 얻는다. -unit path/file path 대응은 host filesystem의 기본 case 규칙이 아니라 이 규범적 비교를 -써서 DOS와 모든 host에서 같게 처리한다. suffix가 일치하지 않으면 컴파일 에러이며 같은 -unit 선언을 임의 위치에서 조용히 허용하지 않는다. case-sensitive host에 `game/main.fe`와 -`GAME/MAIN.FE`가 별도 실제 파일로 함께 있으면 §8.2의 서로 다른 후보이므로 ambiguous다. - -### 8.2 import root와 모호성 - -candidate root는 다음 집합이다. - -1. entry source에서 계산한 project source root -2. 사용자가 지정한 각 `-I ` -3. `std.*`에 대해서만 compiler 내장 std root - -`std` 최상위 namespace는 compiler-reserved이며 user unit은 선언할 수 없다. 일반 user -unit은 compiler std root에서 찾지 않는다. 표준 유닛은 `import std.io;`, -`import std.mem;`, `import std.fmt;`, `import std.sys;`처럼 가져온다. `str`은 계속 built-in -`[]u8` alias와 alias-method namespace이며 import unit이 아니다. - -root를 순서대로 검사해 첫 성공을 고르지 않고 모든 candidate를 조사한다. 동일 canonical -unit path에 대해 서로 다른 실제 source file이 둘 이상 발견되면 다음 형태의 compile -error를 내고 각 실제 path를 note로 표시한다. - -``` -ambiguous unit 'foo.bar' -note: ... -note: ... -``` - -filesystem canonicalization 결과 같은 실제 파일이 여러 root 또는 path alias로 발견된 -경우만 하나로 취급할 수 있다. 따라서 `-I` 순서, 디렉터리 열거 순서, host 환경이 source -선택을 바꾸지 않는다. - -### 8.3 binding, visibility와 외부 library +### 8.2 binding과 visibility 한 unit에서 import binding은 다른 unit-scope declaration/import binding과 충돌할 수 없다. `import foo.net; import bar.net;`은 둘 다 `net`을 만들므로 에러이며 두 번째를 @@ -679,74 +645,12 @@ function parameter/return, public field 등 외부 signature에 나타나는 nom importer가 이름을 해석할 수 있어야 하며 private nominal type을 public API에 노출하면 컴파일 에러다. -v0.1 외부 library는 source-only import root다. 예를 들어 `-I deps`와 -`deps/tinyjson/parse.fe`가 있으면 `import tinyjson.parse;`로 사용한다. package manager, -registry, version solver, manifest dependency 문법은 v0.1에 없다. 미래 package manager도 -dependency source tree를 import root에 배치하고 `-I`를 구성하는 도구일 뿐 Ferro import -의미론을 바꾸지 않는다. `.fei + .obj/.lib`만 배포하는 binary-only package ABI도 v0.1은 -지원하지 않는다. - -### 8.4 `.fei` interface와 cache - -`.fei`는 incremental compilation interface, build cache metadata, 다른 unit에서의 generic -instantiation을 위한 compiler interface다. 물리적 binary/text encoding은 구현 세부지만 -논리적으로 최소한 다음을 표현할 수 있어야 한다. - -- magic/format version, Ferro SPEC/compiler interface version -- target과 해당하는 경우 bits16 memory model -- canonical unit name -- public symbol signature와 public nominal type identity/layout -- anonymous `error.Name` name set -- exported generic declaration metadata와 body token stream -- exported generic이 요구하는 private support symbol metadata -- direct dependency canonical unit name과 dependency interface hash - -`.fei` serialization은 결정적이어야 한다. unordered container iteration을 그대로 쓰지 -않고 canonical key/name의 byte ordering으로 정렬해 serialize한다. absolute host path, -timestamp, build directory를 기록하지 않는다. 같은 source/interface graph와 target/model이면 -build order와 host path에 관계없이 byte-identical `.fei`가 목표다. - -다음 hash는 구별한다. - -- **source hash**: 해당 unit source 내용 변화 감지 -- **interface hash**: dependent unit이 관찰하는 `.fei` 의미 정보의 hash -- **compile cache key**: source hash, target/model/options와 실제 재컴파일에 필요한 - direct/indirect dependency interface hash를 포함한 key - -private non-generic 구현만 바뀌어 public/generic-visible interface가 같으면 해당 unit은 -재컴파일하지만 interface hash는 유지되어 dependent unit을 재컴파일하지 않아도 된다. -public signature/layout 또는 exported generic이 관찰하는 private support 정보가 바뀌면 -interface hash가 바뀐다. - -### 8.5 순환과 canonical identity +### 8.3 순환과 canonical identity 순환 import는 컴파일 에러다. fully-qualified dotted unit path와 선언 이름이 nominal identity의 기준이므로 `tinyjson.value.Value`와 `tinyjson.value.Box`처럼 표시한다. nominal struct/enum/error는 defining unit + declaration name으로 구별되고 type alias는 새 nominal -identity를 만들지 않는다. 이 canonical identity 규칙은 §9 generic cache와 §11.4 C -mangling에도 그대로 적용한다. - -``` -fec main.fe --target=bits32 -o game.exe -fec main.fe --target=bits16 --model=large --no-checks -o game.exe -fec main.fe --emit-c -o out/ # 트랜스파일 결과만 -fec --dump-ast main.fe -``` - -### 8.6 CLI 플래그 (전체) - -| 플래그 | 의미 | 규정 | -|---|---|---| -| `--target=bits16\|bits32` | 타깃 선택 | §2 | -| `--model=small\|large` | `bits16` 메모리 모델 | §2 | -| `-o <경로>` | 출력 파일 또는 디렉터리 | §8 | -| `-I <디렉터리>` | source-only import candidate root 추가 | §8.2·§8.3 | -| `--emit-c` | 트랜스파일 결과만 생성 | §8 | -| `--dump-ast` | AST 덤프 | §8 | -| `--no-checks` | 경계·오버플로·`.?` 검사 제거 | §7.4 | -| `--strip-error-names` | 실행 파일에서 에러 이름 문자열 제거 | §4.6 | -| `--error-table=<파일>` | 유닛 단위 `--emit-c`용 확정 에러 코드 표 | §4.6 | -| `--deny-recursive-drop` | 재귀 drop 경고를 에러로 승격 | §5 R3 | +identity를 만들지 않는다. --- @@ -798,16 +702,9 @@ var xs: List(u8) = List(u8).new(); identity + canonical type argument list**다. type alias는 새 nominal identity가 아니므로 underlying/interned canonical type identity로 정규화한다. 따라서 `const Word = i32;` 뒤의 `id(Word, 1)`과 `id(i32, 2)`는 같은 instance다. -- 최종 build driver는 모든 unit의 instance request를 모아 canonical key로 중복 제거하고 - key의 byte ordering으로 정렬한다. 먼저 필요한 prototype을 결정적 순서로 방출하고 이어서 - body를 같은 순서로 단일 `fe_generics.c`에 방출한다. request 발견 순서, hash iteration, - build order에 의존하거나 사용 unit별 external/static 중복 코드를 만들지 않는다. -- exported generic이 definition unit의 private symbol을 참조하면 `.fei`는 다른 unit에서 - instantiate하는 데 필요한 support dependency의 transitive closure를 기록한다. private - non-generic function은 signature와 backend link identity, private nominal type은 필요한 - identity/layout/signature, comptime const는 evaluated value/type, private generic은 body - token stream과 자기 support dependency를 제공한다. 이 compiler/link metadata는 Ferro - source visibility를 public으로 바꾸지 않는다. +- exported generic은 definition unit의 private symbol을 참조할 수 있다. 이는 컴파일러 + 수준의 처리이며 Ferro source visibility를 public으로 바꾸지 않는다 — 다른 Ferro source는 + 여전히 그 private 심볼을 직접 참조할 수 없다. ```fe unit lib; @@ -820,8 +717,8 @@ pub fn bump(comptime T: type, x: T) -> T { } ``` -다른 unit이 요청한 `lib.bump(i32)` instance는 generated internal C symbol을 통해 -`helper`를 호출할 수 있지만, 다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다. +다른 unit이 요청한 `lib.bump(i32)` instance는 내부적으로 `helper`를 호출할 수 있지만, +다른 Ferro source가 `lib.helper`를 직접 참조할 수는 없다. - 재귀적 인스턴스화의 distinct-instance chain 제한은 32다. 이미 pending/known인 동일 canonical instance key를 다시 요청하는 recursion은 pending instance를 재사용하고 depth를 @@ -832,264 +729,31 @@ pub fn bump(comptime T: type, x: T) -> T { --- -## 10. 표준 라이브러리 (최소 집합) +## 10. 표준 라이브러리 -표준 라이브러리는 reserved `std` namespace 아래에 있으며 `import std.io;`처럼 명시적으로 -가져온다. import 뒤의 local binding은 마지막 segment라 기존처럼 `io.write`, `mem.replace` -형태로 사용한다. 실제 `fec/std` source 배치는 M8에서 이 canonical unit path에 맞춘다. +표준 라이브러리 상세 명세는 별도 문서에서 다룬다. 표준 라이브러리는 reserved `std` +namespace 아래에 있으며 `import std.io;`처럼 명시적으로 가져온다. import 뒤의 local +binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다. -- **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합), `assert`. -- **`std.mem`**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다. -- **문자열/바이트**: `str`은 `[]u8` alias다. 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr`를 `line.trim()`처럼 호출하며 `str` 이름의 import 유닛은 두지 않는다. 소유 문자열 `String`은 `^[]u8`을 감싸고 `as_str(self: &Self) -> str`을 제공한다. -- **`std.list`**: `List(T)`. -- **`std.map`**: `Map(K, V)`(오픈 어드레싱, K는 정수 또는 `String`). `String` key map은 key buffer를 소유하고 조회에는 `get_str(self: &Self, key: str) -> ?&V`를 제공한다. -- **`std.fmt`**: sink를 소유하지 않는 순수 변환 함수 모음. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error`는 `--strip-error-names`를 따른다. +아래는 언어 규칙(§4~§7)이 직접 참조하거나 언어 표면(빌트인, 예제)이 전제하는 최소 +표면만 남긴 것이다. `std.list`, `std.map`, `std.io.File`의 전체 API, `std.sys`의 OS +접근 함수 등 나머지 모듈의 정확한 시그니처는 표준 라이브러리 명세가 정의한다. + +- **`std.core`**: `panic`, `set_panic_handler`, `Error`(기본 에러 집합, §4.6), `assert`. `panic`/`set_panic_handler`는 §7.4 트랩 동작이 참조한다. +- **`std.mem`**: `create(value: T) -> !^T`(T는 값에서 추론), `destroy(p)`, `alloc_slice(T, n) -> !^[]T`, `replace(dst: &mut T, value: T) -> T`, `copy(dst: []mut u8, src: []u8)`, `set(dst: []mut u8, v: u8)`, `Arena{ init, alloc, reset, drop }`. 초기화되지 않은 힙을 안전 코드에 반환하는 `create(T)` 형태는 없다. `replace`는 이전 값을 이동해 반환하고 새 값으로 자리를 초기화하며 부분 이동과 재귀 구조의 반복 drop에 사용한다(§4.5, §5 R3·R7·R11). +- **문자열/바이트**: `str`은 `[]u8` alias다(§4.2). 내장 alias 메서드 `eq`, `find`, `starts_with`, `split_at`, `parse_int`, `trim`, `to_cstr`, `from_cstr`를 `line.trim()`처럼 호출하며(§6.4 예제) `str` 이름의 import 유닛은 두지 않는다. +- **`std.fmt`**: sink를 소유하지 않는 순수 변환 함수 모음이며 `@print`/`@fprint`/`@sprint`(§6.3.1)가 의존한다. `fmt_int_i8/i16/i32/u8/u16/u32(buf: []mut u8, v) -> str`, `fmt_hex_*`, `fmt_char`, `fmt_bool`, `fmt_error`, `fmt_int_pad`를 제공한다. 반환 slice는 buf에서 파생된 R8(a) 결과다. `fmt_error`는 `--strip-error-names`를 따른다. - **`std.io`**: ```fe pub enum Writer { Stdout, Stderr, File(u16), Null } pub enum Reader { Stdin, File(u16) } ``` - 둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다. `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다. 닫힌 fd 또는 재사용된 fd를 가진 복사 handle은 I/O 오류나 의도하지 않은 파일 접근이라는 논리 오류를 만들 수 있지만 dangling memory access는 만들지 않는다. - - `File{ open, create, read(self: &mut Self, []mut u8), write(self: &mut Self, []u8), seek, size, writer, reader, close }`. - - `close(self: Self) -> !void`는 File을 소비하는 일반 메서드이며 `drop`이 아니다. 내부 handle을 먼저 invalid 상태로 만든 뒤 닫기 오류를 반환하므로 함수 종료의 자동 drop은 no-op이다. `drop`은 아직 열린 handle만 오류를 무시하고 닫는다. `drop` 직접 호출 금지는 유지한다. - - 안전한 표준 라이브러리 API는 대여 대상을 가리키는 raw pointer를 값에 숨겨 반환해서는 안 된다. 따라서 v0.1에는 함수 포인터/`*void` 기반 Writer·Reader나 buffer Writer가 없다. `@sprint`는 대상 slice에 직접 복사한다. -- **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`, `args`, `env`, `ticks`, `int21(regs)`, `dpmi_*`(bits32), `port_in/out`, `far_copy`(bits16). `on_exit`은 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. + 둘 다 정수 payload만 가진 Copy handle이며 참조나 raw context pointer를 저장하지 않는다(§5 R8 예제). `io.write(w: Writer, buf: []u8) -> !usize`, `io.read(r: Reader, buf: []mut u8) -> !usize`가 실제 I/O를 수행한다. +- **`std.sys`**: `exit`, `on_exit(f: fn() -> void) -> !void`. `on_exit`은 §7.4 트랩 동작이 참조하는 allocation 없는 고정 크기 callback registry이며 가득 차면 오류를 반환한다. --- -## 11. 컴파일러 구현 - -### 11.1 부트스트랩 전략 - -1. **컴파일러 A** — C89로 작성. Ferro → C 트랜스파일러. 호스트는 현대 PC 또는 DOS. 출력 C는 DJGPP(gcc, bits32) / Open Watcom(bits16, bits32) / Borland C(bits16)로 컴파일. -2. **컴파일러 B** — Ferro로 A와 동일 구조를 재작성. A로 빌드. -3. **셀프호스팅** — B로 B를 빌드. 그 결과로 다시 B를 빌드해 출력이 바이트 동일(fixpoint)하면 완료. A 폐기. -4. **네이티브 백엔드** — B에 386 코드 생성기 추가, 이후 8086 코드 생성기. - -A는 버릴 코드다. 최적화하지 말고 B를 컴파일할 수 있는 최소 언어 부분집합만 지원한다. - -### 11.2 파이프라인 - -``` -소스 → lexer → parser(AST) → resolve(이름/import) → check(타입) - → own(소유권·참조) → lower(소멸자/defer/try/for 전개 → LIR) - → emit_c(C 소스) [또는 emit_x86] -``` - -각 단계는 실패해도 가능한 한 진행해 에러를 모아 보고한다(문장 단위 복구). - -### 11.3 디렉터리 - -``` -fec/ - src/ - lexer.c/h 토큰화. 위치(파일, 줄, 열) 보존. - ast.c/h 노드 정의, 아레나 할당자. - parser.c/h LL(1) 재귀하강. 에러 복구는 다음 ';' 또는 '}'까지 스킵. - types.c/h 타입 인터닝(포인터 비교로 동등성), 레이아웃 계산(타깃별). - resolve.c/h 스코프 체인, 심볼 테이블, import, .fei 읽기/쓰기. - check.c/h 타입 검사, 리터럴 타입 결정, match 완전성, R4 위치 검사. - own.c/h §11.5 알고리즘. - lower.c/h AST → LIR. 소멸자/defer 삽입, try/catch/for/메서드 호출 전개. - emit_c.c/h LIR → C. §11.4 규칙. - generic.c/h 인스턴스 캐시, 토큰 재파싱. - driver.c CLI, 유닛 의존 순서, .fei 캐시, fe_errors.h와 fe_generics.c 생성, - 외부 C 컴파일러 호출. - rt/ 런타임 (C): trap, 힙, 슬라이스 헬퍼, DPMI/INT21 shim - std/ 표준 라이브러리 (.fe) - tests/ §12 -``` - -위는 목표 구조다. M5까지는 이름 해석·소유권·lower가 `check.c`/`emit_c.c`에 통합되어 -있다. R1~R8 전체를 다루는 M6 착수 시점에 `own.c/h`를 분리한다. - -### 11.4 C 방출 규칙 - -| Ferro | C | -|---|---| -| `i16`, `u32` 등 | `int16_t`, `uint32_t` (`` 없으면 자체 typedef) | -| `usize` | `uint16_t`(bits16) / `uint32_t`(bits32) | -| `bool` | `unsigned char` | -| 일반 `^T`, `*T` | `T*` | -| `^[]T` | `typedef struct { T* p; fe_usize n; } fe_owned_slice_T;` | -| `&T` | `const T*` | -| `&mut T` | `T*` | -| `far X` | `__far X` (Watcom/Borland), bits32는 무시 | -| `[N]T` | `struct { T a[N]; }` (값 의미론 유지, 붕괴 방지) | -| `[]T`/`str` | `typedef struct { const T* p; fe_usize n; } fe_slice_T;` | -| `[]mut T` | `typedef struct { T* p; fe_usize n; } fe_mut_slice_T;` | -| `?T` (포인터류) | 원래 포인터, null 사용 | -| `?^[]T` | `struct { unsigned char has; fe_owned_slice_T v; }` | -| `?T` (그 외) | `struct { unsigned char has; T v; }` | -| `E!T` | `struct { uint16_t e; T v; }`, `!void`는 `uint16_t` | -| `shared [atomic] var x: T` | `volatile T x` | -| struct | `struct fe__` | -| enum | `struct { uint8_t tag; union { ... } u; }`, 배리언트 256개 초과 시 `uint16_t tag` | -| 함수 | `fe__`, 메서드는 `fe___` | -| 제네릭 인스턴스 | `fe____` | - -세부: -- **canonical mangling**: C symbol은 canonical dotted unit path + declaration name + canonical - type argument list를 collision-free하게 encode한다. `.`의 separator/escaping 문자는 구현 - 세부지만 host path, pointer address, insertion/hash iteration order를 사용할 수 없다. - type alias는 canonical underlying identity를 쓰고 nominal struct/enum/error는 fully-qualified - defining unit + name을 쓴다. generic instance 이름도 같은 key에서만 생성해 M12 fixpoint에서 - byte-identical해야 한다. -- **Ferro visibility와 C linkage**: Ferro `private`는 source name visibility이며 반드시 C - `static`을 뜻하지 않는다. exported generic instance가 definition unit의 private helper를 - 호출할 수 있도록 generic body가 필요로 하는 private top-level function/global을 - deterministic unit-mangled external C symbol로 방출하고 내부 generated header에 prototype을 - 제공할 수 있다. 이는 resolver의 private 접근을 완화하지 않으며 다른 Ferro unit source의 - 직접 참조는 계속 에러다. 필요한 closure는 §9와 `.fei` metadata가 제공한다. -- **오버플로 검사**: `fe_add_i16(a, b, LINE)` 인라인 함수. `--no-checks`면 매크로가 `((a)+(b))`로 축약. -- **경계 검사**: `fe_idx_T(s, i, LINE)` → `(i < s.n ? s.p[i] : (fe_trap_bounds(LINE), s.p[0]))`. `for` 루프는 직접 인덱스. -- **`try`**: `{ Ttmp t = expr; if (t.e) return (RetT){ t.e }; }` 후 `t.v` 사용. defer/소멸자가 있으면 return 전에 정리 코드 삽입. -- **`catch`**: `t.e`가 참일 때만 block 또는 짧은 RHS를 평가하고 바인딩 변수는 `t.e`다. -- **`defer`/소멸자**: lower 단계에서 스코프 종료 지점(정상 흐름, `return`, `break`, `continue`, `try` 전파)마다 역순 호출을 명시적으로 삽입. C의 goto 라벨을 써도 되고 복제해도 된다(A는 복제, B는 goto 권장). -- **조건부 이동**: 이동 여부가 분기에 따라 다르면 `unsigned char fe_live_ = 1;` 플래그 삽입, drop 전에 검사. -- **`match`**: `switch (x.tag)`. Copy payload는 지역 변수로 복사하고 non-Copy projection payload는 R7에 따라 참조로만 바인딩한다. 소유값 추출은 match 전 `mem.replace`로 수행한다. -- **`asm`**: Intel 문법으로 고정 저장. Watcom/Borland는 그대로, gcc는 `__asm__(".intel_syntax noprefix\n" ...)`로 감싼다. -- **`@print` 계열**: emit 단계에는 도달하지 않는다. lower 단계에서 `fmt.fmt_*`로 임시 `[]mut u8`에 변환하고 `io.write` 또는 `mem.copy`를 호출하는 나열로 전개한다. `@print`는 각 I/O 오류를 버리고, `@fprint`는 첫 오류를 전파하며, `@sprint`는 남은 길이를 추적해 잘라 쓴 실제 길이를 반환한다. 포맷 문자열 조각은 static const shared slice로 방출하고 동일 문자열은 중복 제거한다. -- **참조와 aliasing**: `&T` → `const T*` 방출은 aliasing 가정을 하지 않는다. `&mut T`에도 `restrict`를 붙이지 않으며, M13/M14 네이티브 백엔드도 noalias를 가정하지 않는다. R6의 배타성은 R10의 전역 대여 금지가 함께 성립할 때만 프로그램 전체에서 유지되므로, 방출 단계에서 이를 최적화 근거로 쓰지 않는다. -- **에러 코드**: 드라이버가 emit 전에 확정한 `error.Name`의 `u16` 코드를 단일 `fe_errors.h`의 `#define`으로 방출한다(§4.6). 모든 유닛 C가 이 헤더를 include하므로 에러 `match`를 `switch`로 방출할 수 있고 이름 집합 변경 시 C 재방출 없이 오브젝트만 무효화한다. -- **논리 연산**: `and`, `or`, `not`은 각각 C의 `&&`, `||`, `!`로 방출한다. `and`와 `or`는 C의 시퀀스 포인트와 단축 평가를 그대로 사용한다. -- **평가 순서**: §7.6의 callee-first, operand/argument left-to-right 순서를 지킨다. C가 - 순서를 보장하지 않는 일반 호출 인자와 이항 operand에 부수 효과·move·borrow가 있으면 - lower가 순서대로 temporary statement를 만들고 emit은 그 결과만 조합한다. cleanup과 - `try` 전파도 같은 순서를 따른다. -- **공유 상태와 임계 구역**: `shared`는 `volatile`로 방출한다. bits16의 `critical`은 compiler barrier → FLAGS 저장 → `cli` 순서로 진입하고 모든 이탈에서 저장한 FLAGS 복원 → compiler barrier 순서로 끝낸다. GCC 계열은 `asm volatile("" ::: "memory")`, Open Watcom/Borland는 optimizer가 내용을 볼 수 없는 별도 runtime 함수 호출 경계를 사용한다. 한 명령 크기의 `shared atomic` 단일 접근은 volatile load/store만 방출한다. -- **방출 순서**: `fe_errors.h` → typedef 전방선언 → struct 정의(동률을 canonical name으로 - 끊는 의존 위상 정렬) → 전역 → 함수 프로토타입 → 함수 본문 → 통합 `fe_generics.c`. - unordered container나 source 발견 순서에 기대지 않는다. `fe_generics.c`는 canonical - instance prototype을 먼저, body를 나중에 각각 key byte ordering으로 방출한다. -- 유닛 하나당 `.c` 하나, `.fei`에서 필요한 부분은 `.h`로 생성한다. 제네릭 인스턴스 본문은 유닛 C에 중복 방출하지 않는다. - -### 11.5 own.c 알고리즘 - -함수 단위. 각 지역 변수/파라미터에 상태: -``` -Uninit | Owned | Moved | MaybeMoved | Shared(n) | Exclusive -``` - -초기화 여부가 경로마다 다른 합류에는 `MaybeUninit` 또는 같은 의미의 별도 bit/state를 -사용할 수 있다. projection은 root local/parameter를 찾는 데만 사용하고 field/index별 -대여 상태는 만들지 않는다(R6). - -1. 먼저 AST를 역방향 순회해 각 참조 변수의 경로별 마지막 사용을 계산한다. `defer` 안의 사용은 해당 스코프 끝으로 올린다. -2. AST를 §7.6의 평가 순서로 순회하며 상태 전이한다. 표현식의 place 사용을 읽기 / 이동 / `&` 대여 / `&mut` 대여 / 쓰기 / projection으로 분류하고 projection의 root 상태를 갱신한다. -3. 이동: `Owned → Moved`. `Moved`/`MaybeMoved` 사용 시 에러(최초 이동 위치를 표시). field/index/`.?` projection의 비-Copy 이동은 R7에 따라 거부하고 `mem.replace`만 허용한다. -4. `&place`: root의 `Owned → Shared(n+1)`. `&mut place`: root의 `Owned → Exclusive`. 역방향 pass가 계산한 마지막 사용에서 해제하며 임시는 문장 끝에 해제한다. 서로 다른 field/index도 같은 root 상태와 충돌한다. -5. 호출 인자의 `&mut → &`, `[]mut → []`만 Exclusive를 유지하는 호출 기간의 임시 shared view로 검사한다. 일반 `let`/대입에는 이 implicit transition을 적용하지 않는다. -6. `Shared`/`Exclusive` 상태에서 금지된 쓰기/이동/재대여를 진단한다(R6). -7. **분기 합류**: `if`/`match`의 각 branch를 독립 상태로 계산한다. branch exit 전에 last-use/liveness에 따라 끝난 borrow를 먼저 해제하고 다음 규칙으로 병합한다. - - | 왼쪽 | 오른쪽 | 결과 | - |---|---|---| - | 같은 상태 | 같은 상태 | 같은 상태 | - | `Owned` | `Moved` | `MaybeMoved` | - | `Moved` | `Owned` | `MaybeMoved` | - | `MaybeMoved` | `Owned`/`Moved`/`MaybeMoved` | `MaybeMoved` | - | `Uninit` | `Owned` 등 initialized 상태 | `MaybeUninit` 또는 동등 상태 | - - merge는 좌우 대칭이다. `MaybeMoved`/`MaybeUninit` 값의 이후 읽기·이동은 에러이고 drop은 필요한 runtime live - flag를 쓴다. 한 경로에서만 borrow가 계속 살아 있으면 합류 뒤에도 살아 있는 것으로 - 보수적으로 취급한다. `Shared(n)`과 `Shared(m)`은 필요한 live shared borrow의 합집합을 - 보존하고 단순 count 구현에서는 적어도 `Shared(max(n,m))`으로 합친다. 한쪽만 - `Exclusive`가 live여도 합류 뒤 root를 `Exclusive` 효과로 잠근다. 서로 양립할 수 없는 - `Shared`/`Exclusive` 상태는 더 약한 상태로 풀지 않고 양쪽 효과를 보존하는 보수적 - 상태로 합치거나 compile error를 낸다. 구현은 state enum/bitset을 확장할 수 있지만 이 - 의미를 만족해야 한다. -8. **루프 fixed point**: loop 진입 상태로 body를 한 번 분석하고 종료/backedge 상태를 - 진입 상태와 위 규칙으로 병합한다. 그 merged 상태로 body를 두 번째 분석한다. v0.1 - compiler A는 이 2-pass를 사용하며 두 번째 분석 뒤에도 의미 상태가 안정되지 않으면 - compile error다. loop 밖에서 생성되어 loop 안에서 이후 사용되는 borrow는 필요하면 - loop 전체에 걸쳐 live로 보고, body에서 생성된 borrow가 backedge를 넘는 경우도 같은 - fixed-point에 포함한다. 첫 iteration만 안전하다는 이유로 허용하지 않는다. -9. R4 위반은 check 단계에서 타입만 보고 거부한다. 단 `static str`은 initializer가 문자열 리터럴인지 함께 확인한다. -10. R8 반환 경로마다 `Static`/`Param(N)` provenance를 계산하고 §5 R8 lattice로 합류한다. - 결과 바인딩을 허용하며 `Param(N)` 원본 대여를 결과의 마지막 사용까지 전파한다. - 메서드는 `Param(self)`만, 자유 함수는 유일한 참조성 parameter의 `Param(N)`만 허용하고 - provenance를 lowered signature/`.fei`에 기록한다. - -에러 메시지 형식: `file:line:col: error: <설명>` + 관련 위치 `file:line:col: note: <최초 이동/대여 위치>`. - -### 11.6 마일스톤 - -| # | 내용 | 완료 기준 | -|---|---|---| -| M1 | lexer, parser, AST 덤프 | `--dump-ast`가 std 소스 전체를 파싱 | -| M2 | 타입 검사 + C 방출: 정수, 함수, if, while | bits32 hello world 실행 | -| M3 | struct, enum, match, 배열, `[]T`/`[]mut T`, 경계 검사, `str` alias | 공유/배타 슬라이스와 문자열 처리 예제 통과 | -| M4 | **`@print`/`@fprint`/`@sprint` 빌트인** (§6.3.1), handle enum `io.Writer`, 순수 `fmt.fmt_*` | `@print` 오류 삼킴, `@fprint` 전파, `@sprint` 잘림/길이와 인자 타입 진단, safe Writer dangling 불가 | -| M5 | `^T`/`^[]T`, drop, defer, 이동·부분 이동 검사 | 누수/이중해제, 소비 close, `mem.replace` 테스트 통과 | -| M6 | `&`, `&mut`, 배타성 검사 (own.c 전체) | R1~R8 실패 테스트 통과 | -| M7 | `?T`, `E!T`, try/catch | `std.io` 유닛 동작 | -| M8 | 유닛/import/.fei, 의존 hash, `fe_errors.h`, 분리 컴파일, std 초안 | 다중 유닛 증분·결정적 빌드 | -| M9 | **제네릭** (통합 모노모피제이션) | `fe_generics.c`로 `List(T)`, `Map(K,V)` 중복 없이 빌드 | -| M10 | bits16 타깃: far, `@seg_ptr`, 메모리 모델, asm, interrupt fn, `shared`/`atomic`/`critical`/`interrupt_safe` | QEMU FreeDOS에서 자동화된 far 포인터·인터럽트 공유 상태 테스트 통과(VGA 데모는 수동/멀티모달 검증 대상이라 완료 게이트에서 제외) | -| M11 | 컴파일러 B를 Ferro로 작성, A로 빌드 | B가 M1~M10 테스트 통과 | -| M12 | 셀프호스팅 fixpoint | B(B(B)) == B(B) 바이트 동일, A 폐기 | -| M13 | 386 네이티브 백엔드 | gcc 없이 빌드, 컴파일 속도 10배 | -| M14 | 8086 네이티브 백엔드 | Watcom 없이 bits16 빌드 | - -배치 근거: -- **M4(포매팅)를 앞에 두는 이유**: 구현이 작고(check + lower 합쳐 300줄 안팎) 언어 표면에 새 개념을 추가하지 않는다. 이후 모든 마일스톤의 디버깅과 M11의 컴파일러 B 에러 출력이 여기에 의존한다. -- **M9(제네릭)가 M10보다 앞인 이유**: 제네릭 없이 표준 라이브러리를 쓰는 기간을 최소화한다. -- **인터페이스(`dyn`)는 마일스톤에 없다**: 부트스트랩 경로에 불필요하고 타입 시스템 전반에 영향을 준다. §13의 v0.2 1순위로 미룬다. 그때까지 dangling이 불가능한 Copy handle enum `io.Writer`/`io.Reader`를 사용한다. - ---- - -## 12. 테스트 - -``` -tests/ - m/ 마일스톤별 fixture. bad-*.fe는 fail 규약을 따른다 - pass/*.fe + *.expected 컴파일→실행→stdout 비교 - fail/*.fe 첫 줄 "// ERROR::<메시지 일부>" - run16/*.fe bits16 빌드 후 QEMU FreeDOS 실행, 출력 파일 비교 - boot/ A/B 출력 비교, fixpoint 검증 -``` - -- `fail/`은 규칙별 최소 3개: R1(이동 후 사용), R3(직접 drop 호출), R4(필드에 공유/배타 slice, `*[]T`), R5(스코프 초과), R6(배타성 위반), R7(무효화·projection 부분 이동), R8(자유 함수 참조성 파라미터 2개, 메서드의 non-self 파생, 가변성 승격), R9(unsafe 밖 raw 역참조, `*void` 역참조), R10(전역 `var` 대여), match 완전성, 암묵 변환, 타입 불일치, `let`에서 mutable slice 생성. -- 포매팅(§6.3.1) 전용 `fail/` 케이스: `{}` 개수 > 인자 개수, 인자 개수 > `{}` 개수, 미지원 verb(`{q}`), 런타임 값 포맷 문자열, 대응 `fmt_*` 없는 타입, 닫히지 않은 `{`, `try @print(...)`(void). -- 포매팅 `pass/` 케이스: 각 verb 1개 이상, `{{` 이스케이프, 인자 0개, `@print` 오류 삼킴, `@sprint` 길이/잘림, handle enum `@fprint`, 동일 `fmt.fmt_*` 결과 재사용. -- R6·R8 전용 `pass/` 케이스: 참조의 마지막 사용 이후 원본 재접근, 분기별 마지막 사용의 합류, R8(a) 결과를 지역 변수에 바인딩한 뒤 대여 종료 후 원본 접근, R8(b)의 문자열 리터럴 반환, `line.trim()` 형태의 슬라이스 반환 연쇄. -- `@compile_error`, `@as_far_fn`, `@call_far`의 comptime/타깃/unsafe 제약과 `error.Name`의 - `core.Error` 등록, 결정적 `fe_errors.h`, `--strip-error-names`, `fmt.fmt_error`를 각각 pass/fail로 검증한다. -- **M6 필수 edge case**: - - 서로 다른 struct field의 `&mut`라도 같은 root borrow 충돌, 서로 다른 array index도 같은 root borrow 충돌. - - 호출 인자의 `&mut → &`/`[]mut → []` 약화 성공과 일반 `let` binding의 같은 암묵 약화 실패. - - R8 `Static`/`Param(N)` 합류 성공, 서로 다른 `Param` provenance 합류 실패. - - 한 branch에서만 live인 borrow의 보수적 합류, `Owned`/`Moved` 및 초기화 상태 합류. - - loop-carried borrow와 move가 2-pass fixed point에서 안정되는 경우와 불안정해 거부되는 경우. -- **M7 필수 edge case**: - - 문맥 없는 `null` 실패와 parameter/명시 타입으로 결정되는 contextual `null` 성공. - - error declaration의 code 0, 중복 member 이름, 중복 숫자 code 실패. - - `E!T` expected 위치의 success/failure contextual construction과 nominal error 자동 변환 실패. - - `orelse`, 짧은/block `catch`의 lazy side effect·move·borrow. - - non-Copy `Some` pattern이 destructive extraction이 아님을 확인하고 projection 소유 추출에는 `mem.replace`가 필요함을 검증. -- **M8 필수 edge case**: - - dotted unit/import와 alias, 마지막 segment binding, binding conflict. - - unit path/file suffix mismatch, uppercase segment와 8자 초과 segment 거부. - - 서로 다른 root의 동일 canonical unit ambiguity와 같은 canonical file 중복 발견의 dedup. - - reserved `std.*` lookup, 일반 user unit이 builtin std root에서 발견되지 않음. - - private type을 public API에 노출하는 경우 실패와 dotted prefix가 private 권한을 주지 않음. - - private-only non-generic 구현 변경 뒤 dependency interface hash 안정 및 dependent cache hit. - - source/interface graph와 target/model이 같을 때 build order, `-I` order, absolute checkout path가 달라도 byte-identical `.fei`. -- **M9 필수 edge case**: - - value generic과 generic type inference 거부, 명시 type argument 성공. - - alias/underlying type의 instance dedup과 동일 canonical instance body 1회 방출. - - exported generic이 definition-unit private helper/private generic을 호출하는 support closure. - - 같은 instance recursion의 pending 재사용과 distinct growing-instance chain depth 32 초과 실패. - - generic request 발견 순서와 build order가 달라도 byte-identical `fe_generics.c`. - - invalid dependent operation의 definition 위치 primary error와 `instantiated here` chain note. -- 각 마일스톤은 해당 기능의 pass/fail 테스트와 함께 완료한다. -- 정식 회귀 실행은 QEMU FreeDOS 내부 Open Watcom의 `TEST-DOS.BAT`로 전 타깃·마일스톤 - gate를 확인한다. host compiler 결과는 편집 보조일 뿐 완료 판정에 사용하지 않는다. - ---- - -## 13. 의도적으로 제외한 기능 +## 11. 의도적으로 제외한 기능 **등급 정의** - `영구` — §1 철학과 정면 충돌. v2.0에서도 넣지 않는다. @@ -1100,7 +764,7 @@ tests/ | 기능 | 등급 | 제외 이유 | 대체 수단 | |---|---|---|---| | 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | Copy handle enum (`io.Writer`, §10) | -| bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 M10 범위 밖 | bits16 실행 파일 또는 polling | +| bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 아직 범위 밖 | bits16 실행 파일 또는 polling | | 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 | | 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 | | 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | @@ -1117,7 +781,7 @@ tests/ | 암묵 형변환 | 영구 | 버그 원인 1위 | `as` | | 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 | -### 13.1 인터페이스 설계 스케치 (v0.2 예정) +### 11.1 인터페이스 설계 스케치 (v0.2 예정) 지금 구현하지 않되, 나중에 `io.Writer` Copy handle enum을 무리 없이 대체할 수 있도록 방향만 고정해 둔다. @@ -1134,7 +798,7 @@ dump(&mut file, buf); // &mut File → &mut dyn Writer 자동 변환 - `dyn I`의 표현은 `(ctx, vtable)` 팻 포인터. vtable은 `(인터페이스, 구현 타입)` 쌍마다 `static const` 하나. - **동적 디스패치 전용.** 제네릭 타입 제약(trait bound)으로는 쓸 수 없다 — 그걸 허용하면 전역 분석이 생긴다. - `&dyn I`는 참조이므로 R4가 적용된다(필드 저장 불가). 필드에 담으려면 `^dyn I`(힙 박싱). -- `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립 마일스톤으로 다룬다. +- `^dyn I`의 drop은 vtable 경유. 이 때문에 `?^dyn I`, drop 전개, 제네릭 인자로서의 `dyn` 등 타입 시스템 여러 곳에 케이스가 추가되므로 독립적으로 다룬다. - 도입 시 `io.Writer`/`io.Reader` handle enum을 `dyn` 기반 API로 교체한다. v0.1 safe API에는 이미 대여 대상을 숨긴 `*void`가 없으므로 이 전환은 기능 확장이지 안전성 수정이 아니다. 위 표에 없는 항목(링크타임 최적화, 디버그 정보 포맷, 언어 서버 등)은 도구 영역이며 v0.2 이후 별도 검토. From fb651529011dc5e46a4ae49f0fad96ca679551fc Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 03:33:23 +0900 Subject: [PATCH 111/184] refactor: keep the front end, drop everything downstream of it The milestone structure had stopped describing the compiler and started shaping it: m7.c, check_m7.c, tests/m2..m9, and a checker and emitter that had each grown past 2,500 lines because there was nowhere else to put anything. Restart from the pipeline instead. What is left is the front end -- lexer, parser, types, ownership, semantic analysis -- and the fixtures that describe it. The C backend, the DOSBox-X runner, the milestone registry and the batch build are removed. The driver now stops after semantic analysis; a code generator attaches where emit_c did. Fixtures move from milestone directories to what they check: parse/ grammar own/ ownership and borrowing types/ type rules optional/ optionals and error unions format/ formatting, try units/ units and visibility generic/ generics pending-backend/ pending-backend/ holds the three fixtures that can only be checked by running a program -- that the bounds check traps, that --no-checks removes it, and that drops and defers actually fire, verified through a fake allocator. Those are not front-end tests and are not pretending to be; they come back first when there is a code generator. tests/run.py replaces the DOSBox-X harness. It builds the front end with the pinned Watcom's Windows-hosted driver and runs every fixture in about two seconds, and it does something the old runner structurally could not: it reads the `// ERROR:line:text` marker each fixture carries and checks the diagnostic against it. Those markers have been in the tree all along, unverified, because DOS could not redirect the compiler's stderr and only the exit code was ever compared. 133/188 pass. The 55 failures are not regressions -- they are what was already true and invisible: - units (27) and generic (23): `import`, `comptime` and generic declarations parse and are then dropped on the floor. No pass looks at them. The old registry did not list these fixtures at all, so nothing said so. - five in own/, optional/ and generic/: a marker disagrees with the diagnostic about the line or the wording. Each is either a wrong marker or a wrong diagnostic and has to be read individually. Everything removed is in git history. --- .gitignore | 3 + fec/build-dos.bat | 49 - fec/src/driver.c | 25 +- fec/src/emit_c.c | 2428 ----------------- fec/src/emit_c.h | 28 - fec/src/lower.c | 227 -- fec/src/lower.h | 61 - fec/tests/{m4 => format}/bad-ari.fe | 0 fec/tests/{m4 => format}/bad-bufw.fe | 0 fec/tests/{m4 => format}/bad-cls.fe | 0 fec/tests/{m4 => format}/bad-many.fe | 0 fec/tests/{m4 => format}/bad-open.fe | 0 fec/tests/{m4 => format}/bad-run.fe | 0 fec/tests/{m4 => format}/bad-try.fe | 0 fec/tests/{m4 => format}/bad-type.fe | 0 fec/tests/{m4 => format}/bad-verb.fe | 0 fec/tests/{m4 => format}/bad-writ.fe | 0 .../{m4/format.fe => format/ok-format.fe} | 0 fec/tests/{m4/prop.fe => format/ok-prop.fe} | 0 .../{m4/try-fpr.fe => format/ok-try-fpr.fe} | 0 fec/tests/{m9 => generic}/README.md | 0 fec/tests/{m9 => generic}/badarg.fe | 0 fec/tests/{m9 => generic}/badarity.fe | 0 fec/tests/{m9 => generic}/badbody.fe | 0 fec/tests/{m9 => generic}/baddepth.fe | 0 fec/tests/{m9 => generic}/baddist.fe | 0 fec/tests/{m9 => generic}/badfew.fe | 0 fec/tests/{m9 => generic}/badinfer.fe | 0 fec/tests/{m9 => generic}/badop.fe | 0 fec/tests/{m9 => generic}/badtype.fe | 0 fec/tests/{m9 => generic}/badvalue.fe | 0 fec/tests/{m9 => generic}/defscope/lib.fe | 0 fec/tests/{m9 => generic}/defscope/main.fe | 0 fec/tests/{m9 => generic}/okalias.fe | 0 fec/tests/{m9 => generic}/okbox.fe | 0 fec/tests/{m9 => generic}/okdedup.fe | 0 fec/tests/{m9 => generic}/okid.fe | 0 fec/tests/{m9 => generic}/okisint.fe | 0 fec/tests/{m9 => generic}/okmulti.fe | 0 fec/tests/{m9 => generic}/oknested.fe | 0 fec/tests/{m9 => generic}/okpair.fe | 0 fec/tests/{m9 => generic}/oksamrec.fe | 0 fec/tests/{m9 => generic}/okscope/lib.fe | 0 fec/tests/{m9 => generic}/okscope/main.fe | 0 fec/tests/{m9 => generic}/okskip.fe | 0 fec/tests/{m9 => generic}/oktypeeq.fe | 0 fec/tests/{m7 => optional}/README.md | 0 fec/tests/{m7 => optional}/badcatch.fe | 0 fec/tests/{m7 => optional}/baddef.fe | 0 fec/tests/{m7 => optional}/baddir.fe | 0 fec/tests/{m7 => optional}/badercod.fe | 0 fec/tests/{m7 => optional}/badernam.fe | 0 fec/tests/{m7 => optional}/badetype.fe | 0 fec/tests/{m7 => optional}/badnull.fe | 0 fec/tests/{m7 => optional}/badoref.fe | 0 fec/tests/{m7 => optional}/badorel.fe | 0 fec/tests/{m7 => optional}/badproj.fe | 0 fec/tests/{m7 => optional}/badqmark.fe | 0 fec/tests/{m7 => optional}/badret.fe | 0 fec/tests/{m7 => optional}/badsome.fe | 0 fec/tests/{m7 => optional}/badtry.fe | 0 fec/tests/{m7 => optional}/badzero.fe | 0 fec/tests/{m7 => optional}/okcatch.fe | 0 fec/tests/{m7 => optional}/okcatmov.fe | 0 fec/tests/{m7 => optional}/okcvoid.fe | 0 fec/tests/{m7 => optional}/okdeflt.fe | 0 fec/tests/{m7 => optional}/okiflet.fe | 0 fec/tests/{m7 => optional}/okmatch.fe | 0 fec/tests/{m7 => optional}/oknull.fe | 0 fec/tests/{m7 => optional}/okorelse.fe | 0 fec/tests/{m7 => optional}/okpatvw.fe | 0 fec/tests/{m7 => optional}/okproj.fe | 0 fec/tests/{m7 => optional}/okrepl.fe | 0 fec/tests/{m7 => optional}/oktrdef.fe | 0 fec/tests/{m7 => optional}/oktry.fe | 0 fec/tests/{m6 => own}/README.md | 0 fec/tests/{m6 => own}/badarg.fe | 0 fec/tests/{m6 => own}/badbinit.fe | 0 fec/tests/{m6 => own}/badbrmov.fe | 0 fec/tests/{m6 => own}/baddefer.fe | 0 fec/tests/{m6 => own}/badfld.fe | 0 fec/tests/{m6 => own}/badglob.fe | 0 fec/tests/{m6 => own}/badgmut.fe | 0 fec/tests/{m6 => own}/badinv.fe | 0 fec/tests/{m6 => own}/badlocsl.fe | 0 fec/tests/{m6 => own}/badloop.fe | 0 fec/tests/{m6 => own}/badmove.fe | 0 fec/tests/{m6 => own}/badmut.fe | 0 fec/tests/{m6 => own}/badmut2.fe | 0 fec/tests/{m6 => own}/badptr.fe | 0 fec/tests/{m6 => own}/badret.fe | 0 fec/tests/{m6 => own}/badrfld.fe | 0 fec/tests/{m6 => own}/badridx.fe | 0 fec/tests/{m6 => own}/badscop.fe | 0 fec/tests/{m6 => own}/badself.fe | 0 fec/tests/{m6 => own}/badshwr.fe | 0 fec/tests/{m6 => own}/badslfld.fe | 0 fec/tests/{m6 => own}/badtwo.fe | 0 fec/tests/{m6 => own}/badup.fe | 0 fec/tests/{m6 => own}/badweak.fe | 0 fec/tests/{m5/defer.fe => own/ok-defer.fe} | 0 fec/tests/{m5/owned.fe => own/ok-owned.fe} | 0 fec/tests/{m6 => own}/okbranch.fe | 0 fec/tests/{m6 => own}/okdefer.fe | 0 fec/tests/{m6 => own}/okglobcp.fe | 0 fec/tests/{m6 => own}/oklast.fe | 0 fec/tests/{m6 => own}/okr8free.fe | 0 fec/tests/{m6 => own}/okr8join.fe | 0 fec/tests/{m6 => own}/okr8meth.fe | 0 fec/tests/{m6 => own}/okr8stat.fe | 0 fec/tests/{m6 => own}/okrebor.fe | 0 fec/tests/{m6 => own}/okrtlast.fe | 0 fec/tests/{m6 => own}/okshare.fe | 0 fec/tests/{m6 => own}/okslreb.fe | 0 fec/tests/{m6 => own}/okstatic.fe | 0 fec/tests/{m6 => own}/oktemp.fe | 0 fec/tests/{m6 => own}/oktrim.fe | 0 fec/tests/{m6 => own}/okwcall.fe | 0 .../{m5/bad-clos.fe => own/own-bad-clos.fe} | 0 .../{m5/bad-cond.fe => own/own-bad-cond.fe} | 0 .../{m5/bad-dbl.fe => own/own-bad-dbl.fe} | 0 .../{m5/bad-dest.fe => own/own-bad-dest.fe} | 0 .../{m5/bad-drop.fe => own/own-bad-drop.fe} | 0 .../{m5/bad-loop.fe => own/own-bad-loop.fe} | 0 .../{m5/bad-move.fe => own/own-bad-move.fe} | 0 .../{m5/bad-proj.fe => own/own-bad-proj.fe} | 0 fec/tests/{pass => parse}/basic.fe | 0 fec/tests/{pass => parse}/keybuilt.fe | 0 fec/tests/{pass => parse}/literals.fe | 0 fec/tests/{fail => parse}/logical.fe | 0 fec/tests/{fail => parse}/misssemi.fe | 0 fec/tests/{fail => parse}/unclcomm.fe | 0 fec/tests/{pass => parse}/v012form.fe | 0 .../bounds-nocheck.fe} | 0 .../bounds-trap.fe} | 0 .../format-prop.c} | 0 .../ownership-drop.c} | 0 .../ownership-drop.fe} | 0 .../slice-bounds-trap.fe} | 0 fec/tests/{m2 => types}/bad-ari.fe | 0 fec/tests/{m2 => types}/bad-asgn.fe | 0 fec/tests/{m2 => types}/bad-cast.fe | 0 fec/tests/{m2 => types}/bad-cond.fe | 0 fec/tests/{m3 => types}/bad-mlet.fe | 0 fec/tests/{m2 => types}/bad-ret.fe | 0 fec/tests/{m3 => types}/bad-shwr.fe | 0 fec/tests/{m2 => types}/bad-type.fe | 0 fec/tests/{m2 => types}/bad-unit.fe | 0 fec/tests/{m2 => types}/bad-unk.fe | 0 fec/tests/{m2 => types}/bad-void.fe | 0 fec/tests/{m3 => types}/badarr.fe | 0 fec/tests/{m3 => types}/badchar.fe | 0 fec/tests/{m3 => types}/badcycle.fe | 0 fec/tests/{m3 => types}/badfield.fe | 0 fec/tests/{m3 => types}/badfld.fe | 0 fec/tests/{m3 => types}/badindex.fe | 0 fec/tests/{m3 => types}/badmat.fe | 0 fec/tests/{m3 => types}/badstr.fe | 0 fec/tests/{m3/array.fe => types/ok-array.fe} | 0 .../{m3/arrayctx.fe => types/ok-arrayctx.fe} | 0 .../{m2/castwhil.fe => types/ok-castwhil.fe} | 0 fec/tests/{m3/char.fe => types/ok-char.fe} | 0 fec/tests/{m3/enum.fe => types/ok-enum.fe} | 0 fec/tests/{m3/for.fe => types/ok-for.fe} | 0 fec/tests/{m2/hello.fe => types/ok-hello.fe} | 0 .../{m3/mutable.fe => types/ok-mutable.fe} | 0 .../{m3/nested.fe => types/ok-nested.fe} | 0 .../{m2/scopes.fe => types/ok-scopes.fe} | 0 fec/tests/{m3/str.fe => types/ok-str.fe} | 0 .../{m3/struct.fe => types/ok-struct.fe} | 0 fec/tests/{m8 => units}/README.md | 0 fec/tests/{m8 => units}/alias/acme/math.fe | 0 fec/tests/{m8 => units}/alias/main.fe | 0 fec/tests/{m8 => units}/badlong/main.fe | 0 fec/tests/{m8 => units}/badupper/main.fe | 0 fec/tests/{m8 => units}/basic/main.fe | 0 fec/tests/{m8 => units}/basic/util.fe | 0 fec/tests/{m8 => units}/bindconf/alpha/net.fe | 0 fec/tests/{m8 => units}/bindconf/beta/net.fe | 0 fec/tests/{m8 => units}/bindconf/main.fe | 0 fec/tests/{m8 => units}/cycle/a.fe | 0 fec/tests/{m8 => units}/cycle/b.fe | 0 fec/tests/{m8 => units}/dotpriv/game/bar.fe | 0 fec/tests/{m8 => units}/dotpriv/game/foo.fe | 0 fec/tests/{m8 => units}/dotpriv/main.fe | 0 fec/tests/{m8 => units}/dotted/acme/math.fe | 0 fec/tests/{m8 => units}/dotted/main.fe | 0 fec/tests/{m8 => units}/errdet/alpha.fe | 0 fec/tests/{m8 => units}/errdet/beta.fe | 0 fec/tests/{m8 => units}/errdet/main.fe | 0 fec/tests/{m8 => units}/errnom/lib.fe | 0 fec/tests/{m8 => units}/errnom/main.fe | 0 fec/tests/{m8 => units}/errsame/alpha.fe | 0 fec/tests/{m8 => units}/errsame/beta.fe | 0 fec/tests/{m8 => units}/errsame/main.fe | 0 fec/tests/{m8 => units}/missing/main.fe | 0 fec/tests/{m8 => units}/privfld/data.fe | 0 fec/tests/{m8 => units}/privfld/main.fe | 0 fec/tests/{m8 => units}/privfn/main.fe | 0 fec/tests/{m8 => units}/privfn/util.fe | 0 fec/tests/{m8 => units}/pubfld/data.fe | 0 fec/tests/{m8 => units}/pubfld/main.fe | 0 fec/tests/{m8 => units}/pubpriv/lib.fe | 0 fec/tests/{m8 => units}/pubpriv/main.fe | 0 fec/tests/{m8 => units}/unitbad/main.fe | 0 src/ferrolang_vm/__init__.py | 1 - src/ferrolang_vm/dos_cli.py | 80 - src/ferrolang_vm/dosboxx.py | 332 --- src/ferrolang_vm/paths.py | 5 - src/ferrolang_vm/registry.py | 260 -- src/ferrolang_vm/suite.py | 16 - src/ferrolang_vm/test_cli.py | 90 - src/node-types.json | 121 - src/parser.c | 1744 ------------ src/tree_sitter/parser.h | 55 - tests/run.py | 140 + tools/README.md | 55 - tools/tests/test_dos_names.py | 72 - tools/tests/test_host_syntax.py | 113 - tools/tests/test_milestones_dosboxx.py | 55 - tools/toolchains/dosboxx.lock.json | 23 - 221 files changed, 149 insertions(+), 5834 deletions(-) delete mode 100644 fec/build-dos.bat delete mode 100644 fec/src/emit_c.c delete mode 100644 fec/src/emit_c.h delete mode 100644 fec/src/lower.c delete mode 100644 fec/src/lower.h rename fec/tests/{m4 => format}/bad-ari.fe (100%) rename fec/tests/{m4 => format}/bad-bufw.fe (100%) rename fec/tests/{m4 => format}/bad-cls.fe (100%) rename fec/tests/{m4 => format}/bad-many.fe (100%) rename fec/tests/{m4 => format}/bad-open.fe (100%) rename fec/tests/{m4 => format}/bad-run.fe (100%) rename fec/tests/{m4 => format}/bad-try.fe (100%) rename fec/tests/{m4 => format}/bad-type.fe (100%) rename fec/tests/{m4 => format}/bad-verb.fe (100%) rename fec/tests/{m4 => format}/bad-writ.fe (100%) rename fec/tests/{m4/format.fe => format/ok-format.fe} (100%) rename fec/tests/{m4/prop.fe => format/ok-prop.fe} (100%) rename fec/tests/{m4/try-fpr.fe => format/ok-try-fpr.fe} (100%) rename fec/tests/{m9 => generic}/README.md (100%) rename fec/tests/{m9 => generic}/badarg.fe (100%) rename fec/tests/{m9 => generic}/badarity.fe (100%) rename fec/tests/{m9 => generic}/badbody.fe (100%) rename fec/tests/{m9 => generic}/baddepth.fe (100%) rename fec/tests/{m9 => generic}/baddist.fe (100%) rename fec/tests/{m9 => generic}/badfew.fe (100%) rename fec/tests/{m9 => generic}/badinfer.fe (100%) rename fec/tests/{m9 => generic}/badop.fe (100%) rename fec/tests/{m9 => generic}/badtype.fe (100%) rename fec/tests/{m9 => generic}/badvalue.fe (100%) rename fec/tests/{m9 => generic}/defscope/lib.fe (100%) rename fec/tests/{m9 => generic}/defscope/main.fe (100%) rename fec/tests/{m9 => generic}/okalias.fe (100%) rename fec/tests/{m9 => generic}/okbox.fe (100%) rename fec/tests/{m9 => generic}/okdedup.fe (100%) rename fec/tests/{m9 => generic}/okid.fe (100%) rename fec/tests/{m9 => generic}/okisint.fe (100%) rename fec/tests/{m9 => generic}/okmulti.fe (100%) rename fec/tests/{m9 => generic}/oknested.fe (100%) rename fec/tests/{m9 => generic}/okpair.fe (100%) rename fec/tests/{m9 => generic}/oksamrec.fe (100%) rename fec/tests/{m9 => generic}/okscope/lib.fe (100%) rename fec/tests/{m9 => generic}/okscope/main.fe (100%) rename fec/tests/{m9 => generic}/okskip.fe (100%) rename fec/tests/{m9 => generic}/oktypeeq.fe (100%) rename fec/tests/{m7 => optional}/README.md (100%) rename fec/tests/{m7 => optional}/badcatch.fe (100%) rename fec/tests/{m7 => optional}/baddef.fe (100%) rename fec/tests/{m7 => optional}/baddir.fe (100%) rename fec/tests/{m7 => optional}/badercod.fe (100%) rename fec/tests/{m7 => optional}/badernam.fe (100%) rename fec/tests/{m7 => optional}/badetype.fe (100%) rename fec/tests/{m7 => optional}/badnull.fe (100%) rename fec/tests/{m7 => optional}/badoref.fe (100%) rename fec/tests/{m7 => optional}/badorel.fe (100%) rename fec/tests/{m7 => optional}/badproj.fe (100%) rename fec/tests/{m7 => optional}/badqmark.fe (100%) rename fec/tests/{m7 => optional}/badret.fe (100%) rename fec/tests/{m7 => optional}/badsome.fe (100%) rename fec/tests/{m7 => optional}/badtry.fe (100%) rename fec/tests/{m7 => optional}/badzero.fe (100%) rename fec/tests/{m7 => optional}/okcatch.fe (100%) rename fec/tests/{m7 => optional}/okcatmov.fe (100%) rename fec/tests/{m7 => optional}/okcvoid.fe (100%) rename fec/tests/{m7 => optional}/okdeflt.fe (100%) rename fec/tests/{m7 => optional}/okiflet.fe (100%) rename fec/tests/{m7 => optional}/okmatch.fe (100%) rename fec/tests/{m7 => optional}/oknull.fe (100%) rename fec/tests/{m7 => optional}/okorelse.fe (100%) rename fec/tests/{m7 => optional}/okpatvw.fe (100%) rename fec/tests/{m7 => optional}/okproj.fe (100%) rename fec/tests/{m7 => optional}/okrepl.fe (100%) rename fec/tests/{m7 => optional}/oktrdef.fe (100%) rename fec/tests/{m7 => optional}/oktry.fe (100%) rename fec/tests/{m6 => own}/README.md (100%) rename fec/tests/{m6 => own}/badarg.fe (100%) rename fec/tests/{m6 => own}/badbinit.fe (100%) rename fec/tests/{m6 => own}/badbrmov.fe (100%) rename fec/tests/{m6 => own}/baddefer.fe (100%) rename fec/tests/{m6 => own}/badfld.fe (100%) rename fec/tests/{m6 => own}/badglob.fe (100%) rename fec/tests/{m6 => own}/badgmut.fe (100%) rename fec/tests/{m6 => own}/badinv.fe (100%) rename fec/tests/{m6 => own}/badlocsl.fe (100%) rename fec/tests/{m6 => own}/badloop.fe (100%) rename fec/tests/{m6 => own}/badmove.fe (100%) rename fec/tests/{m6 => own}/badmut.fe (100%) rename fec/tests/{m6 => own}/badmut2.fe (100%) rename fec/tests/{m6 => own}/badptr.fe (100%) rename fec/tests/{m6 => own}/badret.fe (100%) rename fec/tests/{m6 => own}/badrfld.fe (100%) rename fec/tests/{m6 => own}/badridx.fe (100%) rename fec/tests/{m6 => own}/badscop.fe (100%) rename fec/tests/{m6 => own}/badself.fe (100%) rename fec/tests/{m6 => own}/badshwr.fe (100%) rename fec/tests/{m6 => own}/badslfld.fe (100%) rename fec/tests/{m6 => own}/badtwo.fe (100%) rename fec/tests/{m6 => own}/badup.fe (100%) rename fec/tests/{m6 => own}/badweak.fe (100%) rename fec/tests/{m5/defer.fe => own/ok-defer.fe} (100%) rename fec/tests/{m5/owned.fe => own/ok-owned.fe} (100%) rename fec/tests/{m6 => own}/okbranch.fe (100%) rename fec/tests/{m6 => own}/okdefer.fe (100%) rename fec/tests/{m6 => own}/okglobcp.fe (100%) rename fec/tests/{m6 => own}/oklast.fe (100%) rename fec/tests/{m6 => own}/okr8free.fe (100%) rename fec/tests/{m6 => own}/okr8join.fe (100%) rename fec/tests/{m6 => own}/okr8meth.fe (100%) rename fec/tests/{m6 => own}/okr8stat.fe (100%) rename fec/tests/{m6 => own}/okrebor.fe (100%) rename fec/tests/{m6 => own}/okrtlast.fe (100%) rename fec/tests/{m6 => own}/okshare.fe (100%) rename fec/tests/{m6 => own}/okslreb.fe (100%) rename fec/tests/{m6 => own}/okstatic.fe (100%) rename fec/tests/{m6 => own}/oktemp.fe (100%) rename fec/tests/{m6 => own}/oktrim.fe (100%) rename fec/tests/{m6 => own}/okwcall.fe (100%) rename fec/tests/{m5/bad-clos.fe => own/own-bad-clos.fe} (100%) rename fec/tests/{m5/bad-cond.fe => own/own-bad-cond.fe} (100%) rename fec/tests/{m5/bad-dbl.fe => own/own-bad-dbl.fe} (100%) rename fec/tests/{m5/bad-dest.fe => own/own-bad-dest.fe} (100%) rename fec/tests/{m5/bad-drop.fe => own/own-bad-drop.fe} (100%) rename fec/tests/{m5/bad-loop.fe => own/own-bad-loop.fe} (100%) rename fec/tests/{m5/bad-move.fe => own/own-bad-move.fe} (100%) rename fec/tests/{m5/bad-proj.fe => own/own-bad-proj.fe} (100%) rename fec/tests/{pass => parse}/basic.fe (100%) rename fec/tests/{pass => parse}/keybuilt.fe (100%) rename fec/tests/{pass => parse}/literals.fe (100%) rename fec/tests/{fail => parse}/logical.fe (100%) rename fec/tests/{fail => parse}/misssemi.fe (100%) rename fec/tests/{fail => parse}/unclcomm.fe (100%) rename fec/tests/{pass => parse}/v012form.fe (100%) rename fec/tests/{m3/nochk.fe => pending-backend/bounds-nocheck.fe} (100%) rename fec/tests/{m3/bounds.fe => pending-backend/bounds-trap.fe} (100%) rename fec/tests/{m4/proptest.c => pending-backend/format-prop.c} (100%) rename fec/tests/{m5/runtime.c => pending-backend/ownership-drop.c} (100%) rename fec/tests/{m5/runtime.fe => pending-backend/ownership-drop.fe} (100%) rename fec/tests/{m3/slcbound.fe => pending-backend/slice-bounds-trap.fe} (100%) rename fec/tests/{m2 => types}/bad-ari.fe (100%) rename fec/tests/{m2 => types}/bad-asgn.fe (100%) rename fec/tests/{m2 => types}/bad-cast.fe (100%) rename fec/tests/{m2 => types}/bad-cond.fe (100%) rename fec/tests/{m3 => types}/bad-mlet.fe (100%) rename fec/tests/{m2 => types}/bad-ret.fe (100%) rename fec/tests/{m3 => types}/bad-shwr.fe (100%) rename fec/tests/{m2 => types}/bad-type.fe (100%) rename fec/tests/{m2 => types}/bad-unit.fe (100%) rename fec/tests/{m2 => types}/bad-unk.fe (100%) rename fec/tests/{m2 => types}/bad-void.fe (100%) rename fec/tests/{m3 => types}/badarr.fe (100%) rename fec/tests/{m3 => types}/badchar.fe (100%) rename fec/tests/{m3 => types}/badcycle.fe (100%) rename fec/tests/{m3 => types}/badfield.fe (100%) rename fec/tests/{m3 => types}/badfld.fe (100%) rename fec/tests/{m3 => types}/badindex.fe (100%) rename fec/tests/{m3 => types}/badmat.fe (100%) rename fec/tests/{m3 => types}/badstr.fe (100%) rename fec/tests/{m3/array.fe => types/ok-array.fe} (100%) rename fec/tests/{m3/arrayctx.fe => types/ok-arrayctx.fe} (100%) rename fec/tests/{m2/castwhil.fe => types/ok-castwhil.fe} (100%) rename fec/tests/{m3/char.fe => types/ok-char.fe} (100%) rename fec/tests/{m3/enum.fe => types/ok-enum.fe} (100%) rename fec/tests/{m3/for.fe => types/ok-for.fe} (100%) rename fec/tests/{m2/hello.fe => types/ok-hello.fe} (100%) rename fec/tests/{m3/mutable.fe => types/ok-mutable.fe} (100%) rename fec/tests/{m3/nested.fe => types/ok-nested.fe} (100%) rename fec/tests/{m2/scopes.fe => types/ok-scopes.fe} (100%) rename fec/tests/{m3/str.fe => types/ok-str.fe} (100%) rename fec/tests/{m3/struct.fe => types/ok-struct.fe} (100%) rename fec/tests/{m8 => units}/README.md (100%) rename fec/tests/{m8 => units}/alias/acme/math.fe (100%) rename fec/tests/{m8 => units}/alias/main.fe (100%) rename fec/tests/{m8 => units}/badlong/main.fe (100%) rename fec/tests/{m8 => units}/badupper/main.fe (100%) rename fec/tests/{m8 => units}/basic/main.fe (100%) rename fec/tests/{m8 => units}/basic/util.fe (100%) rename fec/tests/{m8 => units}/bindconf/alpha/net.fe (100%) rename fec/tests/{m8 => units}/bindconf/beta/net.fe (100%) rename fec/tests/{m8 => units}/bindconf/main.fe (100%) rename fec/tests/{m8 => units}/cycle/a.fe (100%) rename fec/tests/{m8 => units}/cycle/b.fe (100%) rename fec/tests/{m8 => units}/dotpriv/game/bar.fe (100%) rename fec/tests/{m8 => units}/dotpriv/game/foo.fe (100%) rename fec/tests/{m8 => units}/dotpriv/main.fe (100%) rename fec/tests/{m8 => units}/dotted/acme/math.fe (100%) rename fec/tests/{m8 => units}/dotted/main.fe (100%) rename fec/tests/{m8 => units}/errdet/alpha.fe (100%) rename fec/tests/{m8 => units}/errdet/beta.fe (100%) rename fec/tests/{m8 => units}/errdet/main.fe (100%) rename fec/tests/{m8 => units}/errnom/lib.fe (100%) rename fec/tests/{m8 => units}/errnom/main.fe (100%) rename fec/tests/{m8 => units}/errsame/alpha.fe (100%) rename fec/tests/{m8 => units}/errsame/beta.fe (100%) rename fec/tests/{m8 => units}/errsame/main.fe (100%) rename fec/tests/{m8 => units}/missing/main.fe (100%) rename fec/tests/{m8 => units}/privfld/data.fe (100%) rename fec/tests/{m8 => units}/privfld/main.fe (100%) rename fec/tests/{m8 => units}/privfn/main.fe (100%) rename fec/tests/{m8 => units}/privfn/util.fe (100%) rename fec/tests/{m8 => units}/pubfld/data.fe (100%) rename fec/tests/{m8 => units}/pubfld/main.fe (100%) rename fec/tests/{m8 => units}/pubpriv/lib.fe (100%) rename fec/tests/{m8 => units}/pubpriv/main.fe (100%) rename fec/tests/{m8 => units}/unitbad/main.fe (100%) delete mode 100644 src/ferrolang_vm/__init__.py delete mode 100644 src/ferrolang_vm/dos_cli.py delete mode 100644 src/ferrolang_vm/dosboxx.py delete mode 100644 src/ferrolang_vm/paths.py delete mode 100644 src/ferrolang_vm/registry.py delete mode 100644 src/ferrolang_vm/suite.py delete mode 100644 src/ferrolang_vm/test_cli.py delete mode 100644 src/node-types.json delete mode 100644 src/parser.c delete mode 100644 src/tree_sitter/parser.h create mode 100644 tests/run.py delete mode 100644 tools/README.md delete mode 100644 tools/tests/test_dos_names.py delete mode 100644 tools/tests/test_host_syntax.py delete mode 100644 tools/tests/test_milestones_dosboxx.py delete mode 100644 tools/toolchains/dosboxx.lock.json diff --git a/.gitignore b/.gitignore index 6979fd4..2ad4b9f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ __pycache__/ node_modules/ .npm/ .cache/ + +# host build output of the front end +.build/ diff --git a/fec/build-dos.bat b/fec/build-dos.bat deleted file mode 100644 index e8c1178..0000000 --- a/fec/build-dos.bat +++ /dev/null @@ -1,49 +0,0 @@ -@echo off -rem Open Watcom C89 build. The runner's generated RUN.BAT calls this from C:\FEC. -C: -cd \FEC -if exist BUILD.OK del BUILD.OK -if exist BUILD.FAIL del BUILD.FAIL -if exist fec.exe del fec.exe -if exist __wcl__.lnk del __wcl__.lnk -if exist *.obj del *.obj - -if "%WATCOM%"=="" set WATCOM=C:\DEVEL\WATCOMC -if not exist %WATCOM%\BINW\WCL.EXE goto build_fail -set PATH=%WATCOM%\BINW;%WATCOM%\BINP;%PATH% -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=arena.obj src\arena.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=diag.obj src\diag.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lexer.obj src\lexer.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=ast.obj src\ast.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=parser.obj src\parser.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=types.obj src\types.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=m7.obj src\m7.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=own.obj src\own.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=check.obj src\check.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=lower.obj src\lower.c -if errorlevel 1 goto build_fail -rem Use an unambiguous short object name for the emitter source. -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=emitc.obj src\emit_c.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -c -fo=driver.obj src\driver.c -if errorlevel 1 goto build_fail -wcl -q -za -wx -bt=dos -ml -k32768 -fe=fec.exe *.obj -if errorlevel 1 goto build_fail -if not exist fec.exe goto build_fail -echo OK>BUILD.OK -cd C:\FEC -goto build_done - -:build_fail -echo FAIL>BUILD.FAIL - -:build_done diff --git a/fec/src/driver.c b/fec/src/driver.c index 90903d5..7a5c55f 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -1,6 +1,5 @@ #include "parser.h" #include "check.h" -#include "emit_c.h" #include #include #include @@ -37,25 +36,20 @@ static void dump_tokens(const char *src, unsigned long n, const char *file, int main(int argc, char **argv) { - int i,dump=0,dump_tok=0,check_only=0,emit=0,no_checks=0; - const char *file=0,*outname=0; + int i,dump=0,dump_tok=0,check_only=0,no_checks=0; + const char *file=0; unsigned long n; char *src; FeDiags d; FeAst ast; FeParser p; FeCheck check; - FeEmitter emitter; - FILE *out; unsigned pointer_bits=32; if(argc<2){usage();return 2;} for(i=1;i=argc){fprintf(fe_diag_stream(),"fec: -o needs a path\n");return 2;}outname=argv[++i];} - else if(strncmp(argv[i],"-o",2)==0 && argv[i][2]) outname=argv[i]+2; else if(strncmp(argv[i],"--target=bits16",15)==0) pointer_bits=16; else if(strncmp(argv[i],"--target=bits32",15)==0) pointer_bits=32; else if(strcmp(argv[i],"--no-checks")==0) no_checks=1; @@ -64,7 +58,7 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"--help")==0){usage();return 0;} else {fprintf(fe_diag_stream(),"fec: unknown option %s\n",argv[i]);return 2;} } - if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)+(emit?1:0)>1){ + if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)>1){ fprintf(fe_diag_stream(),"fec: choose only one output mode\n"); return 2; } @@ -92,16 +86,9 @@ int main(int argc, char **argv) free(src); return 1; } - if(check_only){ - fe_ast_destroy(&ast); - free(src); - return 0; - } - out=outname?fopen(outname,"w"):stdout; - if(!out){fprintf(fe_diag_stream(),"fec: cannot create %s\n",outname);fe_ast_destroy(&ast);free(src);return 2;} - fe_emit_c_init(&emitter,out,&check,pointer_bits,no_checks); - fe_emit_c_program(&emitter); - if(outname)fclose(out); + /* Semantic analysis is the last pass there is. A code generator attaches + here; until then --check and the default path are the same thing. */ + (void)check_only; fe_ast_destroy(&ast); free(src); return d.errors?1:0; diff --git a/fec/src/emit_c.c b/fec/src/emit_c.c deleted file mode 100644 index 1fc185b..0000000 --- a/fec/src/emit_c.c +++ /dev/null @@ -1,2428 +0,0 @@ -#include "emit_c.h" -#include -#include - -static int type_needs_drop(FeType *t); -static void emit_lvalue(FeEmitter *e, FeNode *n); - -static void emit_expr_core(FeEmitter *e, FeNode *n); -static void emit_stmt_core(FeEmitter *e, FeNode *n); -static void emit_block(FeEmitter *e, FeNode *n); - -static void pad(FeEmitter *e) -{ - int i; - for (i = 0; i < e->indent; ++i) fputs(" ", e->out); -} - -static const char *ctype(FeEmitter *e, FeNode *n) -{ - FeType *t; - if (n && n->sem_type) t = n->sem_type; - else if (n) t = fe_type_from_ast(&e->check->types, n); - else t = fe_type_intern(&e->check->types, "i32"); - return fe_type_c_name(t, e->pointer_bits); -} - -static const char *cname(FeNode *n, const char *fallback) -{ - return n && n->cname ? n->cname : fallback; -} - -static void emit_one_type(FeEmitter *e, FeType *t); - - -static void emit_type_deps(FeEmitter *e, FeType *t) -{ - unsigned i,j; - if (!t) return; - if (t->kind == FE_TYPE_ARRAY) emit_one_type(e,t->elem); - if (t->kind == FE_TYPE_STRUCT) - for (i=0;ifield_count;i++) emit_one_type(e,t->fields[i].type); - if (t->kind == FE_TYPE_ENUM) - for (i=0;ivariant_count;i++) - for (j=0;jvariants[i].field_count;j++) - emit_one_type(e,t->variants[i].fields[j].type); - if (t->kind == FE_TYPE_ERROR_UNION && t->error_value) - emit_one_type(e,t->error_value); -} - -static void emit_one_type(FeEmitter *e, FeType *t) -{ - unsigned i,j; - if (t && strcmp(t->name,"io.Writer")==0) return; - if (!t || t->emit_state || - (t->kind != FE_TYPE_STRUCT && t->kind != FE_TYPE_ENUM && - t->kind != FE_TYPE_ARRAY && t->kind != FE_TYPE_SLICE && - !(t->kind == FE_TYPE_OWNED && t->elem && - t->elem->kind == FE_TYPE_SLICE) && - t->kind != FE_TYPE_ERROR_UNION) || - (t->kind == FE_TYPE_ERROR_UNION && - (!t->error_value || t->error_value->kind == FE_TYPE_VOID))) return; - t->emit_state=1; - emit_type_deps(e,t); - if(t->kind==FE_TYPE_STRUCT) { - fputs(t->cname,e->out); fputs(" {\n",e->out); - for(i=0;ifield_count;i++) { fputs(" ",e->out); fputs(fe_type_c_name(t->fields[i].type,e->pointer_bits),e->out); fputc(' ',e->out); fputs(t->fields[i].name,e->out); fputs(";\n",e->out); } - fputs("};\n",e->out); - } else if(t->kind==FE_TYPE_ARRAY) { - fputs(t->cname,e->out); fputs(" { ",e->out); fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" a[",e->out); fprintf(e->out,"%lu",t->length); fputs("]; };\n",e->out); - } else if(t->kind==FE_TYPE_SLICE && t->cname) { - fputs("typedef struct { ",e->out); - if(!t->ref_mut) fputs("const ",e->out); - fputs(fe_type_c_name(t->elem,e->pointer_bits),e->out); fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); fputs(";\n",e->out); - fprintf(e->out,"static %s %s(%s%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n",t->cname,t->maker,t->ref_mut ? "" : "const ",fe_type_c_name(t->elem,e->pointer_bits),t->cname); - } else if(t->kind==FE_TYPE_OWNED && t->elem && - t->elem->kind==FE_TYPE_SLICE) { - FeType *item=t->elem->elem; - fputs("typedef struct { ",e->out); - fputs(fe_type_c_name(item,e->pointer_bits),e->out); - fputs(" *p; unsigned long n; } ",e->out); fputs(t->cname,e->out); - fputs(";\n",e->out); - fprintf(e->out,"static %s %s(%s *p, unsigned long n) { %s s; s.p=p; s.n=n; return s; }\n", - t->cname,t->maker,fe_type_c_name(item,e->pointer_bits),t->cname); - } else if(t->kind==FE_TYPE_ERROR_UNION) { - fputs(t->cname,e->out); fputs(" { unsigned short e; ",e->out); - fputs(fe_type_c_name(t->error_value,e->pointer_bits),e->out); - fputs(" v; } ;\n",e->out); - } else if(t->kind==FE_TYPE_ENUM) { - for(i=0;ivariant_count;i++) if(t->variants[i].field_count>1) { - fprintf(e->out,"struct fe_payload_%s_%s {",t->name,t->variants[i].name); - for(j=0;jvariants[i].field_count;j++) { fputs(" ",e->out); fputs(fe_type_c_name(t->variants[i].fields[j].type,e->pointer_bits),e->out); fputc(' ',e->out); fputs(t->variants[i].fields[j].name,e->out); fputc(';',e->out); } - fputs(" };\n",e->out); - } - fputs(t->cname,e->out); fputs(" { ",e->out); fputs(t->bits>8 ? "unsigned short" : "unsigned char",e->out); fputs(" tag; union { ",e->out); - for(i=0;ivariant_count;i++) { if(t->variants[i].field_count==0) fputs("unsigned char",e->out); else if(t->variants[i].field_count==1) fputs(fe_type_c_name(t->variants[i].fields[0].type,e->pointer_bits),e->out); else fprintf(e->out,"struct fe_payload_%s_%s",t->name,t->variants[i].name); fputc(' ',e->out); fputs(t->variants[i].name,e->out); fputc(';',e->out); } - fputs(" } payload; };\n",e->out); - } - t->emit_state=2; -} - - -static FeNode *find_drop_method(FeEmitter *e, const char *name) -{ - FeNode *n; - FeNode *m; - for (n=e->check->ast->root ? e->check->ast->root->children : 0; n; n=n->next) - if (n->kind==FE_N_STRUCT && n->text && name && strcmp(n->text,name)==0) - for (m=n->children; m; m=m->next) - if (m->kind==FE_N_FN && m->text && strcmp(m->text,"drop")==0) - return m; - return 0; -} - - - -/* SPEC 12.3 lists `trim` among the built-in alias methods on `str`, called as - `line.trim()`. The checker accepts it; this emits the lowering. Only the - helper for slice types actually reached by a trim call is emitted, so a - program that never trims does not carry it. */ -static int node_uses_trim(FeNode *n) -{ - FeNode *x; - if (!n) return 0; - if (n->kind==FE_N_CALL && n->a && n->a->kind==FE_N_MEMBER && - n->a->b && n->a->b->text && strcmp(n->a->b->text,"trim")==0) return 1; - if (node_uses_trim(n->a) || node_uses_trim(n->b) || node_uses_trim(n->c)) - return 1; - for (x=n->children; x; x=x->next) if (node_uses_trim(x)) return 1; - return 0; -} - - -static void emit_m4_runtime(FeEmitter *e) -{ - fputs("typedef struct { unsigned char *p; unsigned long n; } fe_m4_slice;\n",e->out); - fputs("typedef struct { unsigned char tag; unsigned short handle; } fe_writer;\n",e->out); - fputs("unsigned short fe_m4_error;\n",e->out); - fputs("fe_writer fe_m4_writer(unsigned char tag, unsigned short handle) { fe_writer w; w.tag=tag; w.handle=handle; return w; }\n",e->out); - fputs("fe_writer fe_m4_stdout_writer(void) { return fe_m4_writer(0,1); }\n",e->out); - fputs("fe_writer fe_m4_stderr_writer(void) { return fe_m4_writer(1,2); }\n",e->out); - fputs("fe_writer fe_m4_null_writer(void) { return fe_m4_writer(3,0); }\n",e->out); - fputs("/* bounded sprint stack; overflow traps instead of corrupting an outer call */\n#define FE_M4_SPRINT_DEPTH 8\n",e->out); - fputs("typedef struct { fe_m4_slice b; unsigned long start_n; } fe_m4_sprint_frame;\n",e->out); - fputs("static fe_m4_sprint_frame fe_m4_sprint_stack[FE_M4_SPRINT_DEPTH];\n",e->out); - fputs("static unsigned fe_m4_sprint_depth;\n",e->out); - fputs("void fe_m4_sprint_begin(fe_m4_slice *b) { if (fe_m4_sprint_depth>=FE_M4_SPRINT_DEPTH) abort(); fe_m4_sprint_stack[fe_m4_sprint_depth].b=*b; fe_m4_sprint_stack[fe_m4_sprint_depth].start_n=b->n; ++fe_m4_sprint_depth; }\n",e->out); - fputs("fe_writer fe_m4_sprint_writer(void) { return fe_m4_writer(4,(unsigned short)(fe_m4_sprint_depth-1)); }\n",e->out); - fputs("unsigned long fe_m4_sprint_finish(void) { unsigned long result; if (!fe_m4_sprint_depth) abort(); --fe_m4_sprint_depth; result=fe_m4_sprint_stack[fe_m4_sprint_depth].start_n-fe_m4_sprint_stack[fe_m4_sprint_depth].b.n; return result; }\n",e->out); - fputs("unsigned short fe_m4_write_bytes(fe_writer w, const unsigned char *p, unsigned long n) { if(w.tag==0) return fwrite(p,1,(size_t)n,stdout)==(size_t)n?0:1; if(w.tag==1) return fwrite(p,1,(size_t)n,stderr)==(size_t)n?0:1; if(w.tag==3) return 0; if(w.tag==4 && w.handlen?n:b->n; if(k) memcpy(b->p,p,(size_t)k); b->p+=k; b->n-=k; return 0; } return 1; }\n",e->out); - fputs("unsigned short fe_m4_write_cstr(fe_writer w, const char *p) { return fe_m4_write_bytes(w,(const unsigned char*)p,(unsigned long)strlen(p)); }\n",e->out); - fputs("#define fe_m4_write_slice(w,s) fe_m4_write_bytes((w),(s).p,(s).n)\n",e->out); - fputs("unsigned short fe_m4_write_int(fe_writer w, long v) { char b[40]; sprintf(b,\"%ld\",v); return fe_m4_write_cstr(w,b); }\n",e->out); - fputs("unsigned short fe_m4_write_hex(fe_writer w, unsigned long v) { char b[40]; sprintf(b,\"%lx\",v); return fe_m4_write_cstr(w,b); }\n",e->out); - fputs("unsigned short fe_m4_write_char(fe_writer w, unsigned char v) { return fe_m4_write_bytes(w,&v,1); }\n",e->out); - fputs("unsigned short fe_m4_write_bool(fe_writer w, unsigned char v) { return fe_m4_write_cstr(w,v ? \"true\" : \"false\"); }\n",e->out); - fputs("unsigned short fe_m4_write_error(fe_writer w, unsigned long v) { char b[40]; sprintf(b,\"error#%lu\",v); return fe_m4_write_cstr(w,b); }\n",e->out); -} - -static int node_uses_m4(FeNode *n) -{ - FeNode *x; - if (!n) return 0; - if (n->kind==FE_N_CALL && n->text && - (strcmp(n->text,"@print")==0 || strcmp(n->text,"@fprint")==0 || - strcmp(n->text,"@sprint")==0)) return 1; - if (n->kind==FE_N_CALL && n->a && n->a->kind==FE_N_MEMBER && - n->a->a && n->a->a->text && strcmp(n->a->a->text,"io")==0) return 1; - if (node_uses_m4(n->a) || node_uses_m4(n->b) || node_uses_m4(n->c)) return 1; - for (x=n->children; x; x=x->next) if (node_uses_m4(x)) return 1; - return 0; -} - -static void emit_expr(FeEmitter *e, FeNode *n); -static void emit_stmt(FeEmitter *e, FeNode *n); -static void emit_owned_live(FeEmitter *e, FeNode *n, int value); -static void emit_cleanup_all(FeEmitter *e); -static void emit_value_drop(FeEmitter *e, FeNode *n); -static void emit_cleanup_block(FeEmitter *e, FeNode *n); -static void emit_cleanup_to(FeEmitter *e, unsigned floor); -static void emit_param_cleanup(FeEmitter *e); -static void emit_error_return(FeEmitter *e, const char *error_expr); -static void emit_fn(FeEmitter *e, FeNode *fn, int prototype); -static void emit_main_wrapper(FeEmitter *e, FeNode *fn); -static void emit_type_defs(FeEmitter *e); - -static int stmt_definitely_returns(FeNode *n); - -static int match_is_exhaustive(FeNode *n) -{ - FeType *t; - FeNode *arm; - unsigned i; - int found; - if (!n || !n->a) return 0; - t=n->a->sem_type; - if (!t || t->kind!=FE_TYPE_ENUM) return 0; - for (arm=n->children; arm; arm=arm->next) - if (arm->text && strcmp(arm->text,"_")==0) return 1; - for (i=0; ivariant_count; ++i) { - found=0; - for (arm=n->children; arm; arm=arm->next) - if (arm->text && strcmp(arm->text,t->variants[i].name)==0) { - found=1; - break; - } - if (!found) return 0; - } - return 1; -} - -static int match_definitely_returns(FeNode *n) -{ - FeNode *arm; - if (!match_is_exhaustive(n)) return 0; - for (arm=n->children; arm; arm=arm->next) - if (!stmt_definitely_returns(arm->a)) return 0; - return 1; -} - -static int stmt_definitely_returns(FeNode *n) -{ - FeNode *last; - if (!n) return 0; - if (n->kind==FE_N_RETURN) return 1; - if (n->kind==FE_N_MATCH) return match_definitely_returns(n); - if (n->kind==FE_N_BLOCK) { - last=n->children; - if (!last) return 0; - while (last->next) last=last->next; - return stmt_definitely_returns(last); - } - if (n->kind==FE_N_IF) - return n->b && n->c && stmt_definitely_returns(n->b) && - stmt_definitely_returns(n->c); - return 0; -} - -static int hex_value(int c) -{ - if (c>='0' && c<='9') return c-'0'; - if (c>='a' && c<='f') return c-'a'+10; - if (c>='A' && c<='F') return c-'A'+10; - return -1; -} - -static void emit_byte(FILE *out, unsigned value) -{ - fprintf(out,"\\%03o",value & 255U); -} - -static void emit_codepoint(FILE *out, unsigned long cp) -{ - if (cp<=0x7fUL) emit_byte(out,(unsigned)cp); - else if (cp<=0x7ffUL) { - emit_byte(out,(unsigned)(0xc0UL | (cp>>6))); - emit_byte(out,(unsigned)(0x80UL | (cp&0x3fUL))); - } else if (cp<=0xffffUL) { - emit_byte(out,(unsigned)(0xe0UL | (cp>>12))); - emit_byte(out,(unsigned)(0x80UL | ((cp>>6)&0x3fUL))); - emit_byte(out,(unsigned)(0x80UL | (cp&0x3fUL))); - } else { - emit_byte(out,(unsigned)(0xf0UL | (cp>>18))); - emit_byte(out,(unsigned)(0x80UL | ((cp>>12)&0x3fUL))); - emit_byte(out,(unsigned)(0x80UL | ((cp>>6)&0x3fUL))); - emit_byte(out,(unsigned)(0x80UL | (cp&0x3fUL))); - } -} - -static void emit_c_literal(FILE *out, const char *text, int string) -{ - unsigned long i; - unsigned long cp; - int h0,h1,h2,h3; - int c; - char quote=string ? '"' : '\''; - if (!text) { fputs(string ? "\"\"" : "'\\000'",out); return; } - fputc(quote,out); - for(i=1;text[i] && text[i]!=quote;i++) { - c=(unsigned char)text[i]; - if(c!='\\') { - if(c==quote || c=='\\') fputc('\\',out); - fputc(c,out); - continue; - } - ++i; c=(unsigned char)text[i]; - if(c=='u' && text[i+1] && text[i+2] && text[i+3] && text[i+4]) { - h0=hex_value(text[i+1]); h1=hex_value(text[i+2]); - h2=hex_value(text[i+3]); h3=hex_value(text[i+4]); - if(h0>=0 && h1>=0 && h2>=0 && h3>=0) { - cp=(unsigned long)((h0<<12)|(h1<<8)|(h2<<4)|h3); - emit_codepoint(out,cp); i+=4; continue; - } - } - if(c=='x' && text[i+1] && text[i+2]) { - h0=hex_value(text[i+1]); h1=hex_value(text[i+2]); - if(h0>=0 && h1>=0) { emit_byte(out,(unsigned)((h0<<4)|h1)); i+=2; continue; } - } - if(c=='n') emit_byte(out,10U); - else if(c=='r') emit_byte(out,13U); - else if(c=='t') emit_byte(out,9U); - else if(c=='0') emit_byte(out,0U); - else emit_byte(out,(unsigned char)c); - } - fputc(quote,out); -} - -static void emit_m4_piece(FILE *out, const char *fmt, unsigned long begin, - unsigned long end) -{ - unsigned long i; - unsigned long cp; - int h0,h1,h2,h3; - int c; - fputc('"',out); - for(i=begin;i=0 && - (h1=hex_value((unsigned char)fmt[i+2]))>=0) { - emit_byte(out,(unsigned)((h0<<4)|h1)); i+=2; - } else if(c=='u' && i+4=0 && - (h1=hex_value((unsigned char)fmt[i+2]))>=0 && - (h2=hex_value((unsigned char)fmt[i+3]))>=0 && - (h3=hex_value((unsigned char)fmt[i+4]))>=0) { - cp=(unsigned long)((h0<<12)|(h1<<8)|(h2<<4)|h3); - emit_codepoint(out,cp); i+=4; - } - else if(c=='\\' || c=='"') { fputc('\\',out); fputc(c,out); } - else emit_byte(out,(unsigned)c); - continue; - } - if(c=='"' || c=='\\') fputc('\\',out); - fputc(c,out); - } - fputc('"',out); -} - -static void emit_m4_writer(FeEmitter *e, FeNode *arg, int buffer) -{ - (void)buffer; - if (arg && arg->kind==FE_N_UNARY && arg->text && - (strcmp(arg->text,"&")==0 || strcmp(arg->text,"&mut")==0)) { - emit_expr(e,arg->a); - } else if (arg && arg->kind==FE_N_CALL && arg->a && - arg->a->kind==FE_N_MEMBER) { - emit_expr(e,arg); - } else if (arg && arg->sem_type && arg->sem_type->kind==FE_TYPE_REF) { - fputs("(*",e->out); emit_expr(e,arg); fputc(')',e->out); - } else { - emit_expr(e,arg); - } -} - -static void emit_m4_writer_value(FeEmitter *e, FeNode *writer, int buffer) -{ - if (!writer) fputs("fe_m4_stdout_writer()",e->out); - else if (buffer) fputs("fe_m4_sprint_writer()",e->out); - else emit_m4_writer(e,writer,buffer); -} - -static void emit_m4_arg(FeEmitter *e, FeNode *arg, int verb, - FeNode *writer, int buffer, int error_value) -{ - FeType *t=arg ? arg->sem_type : 0; - if (verb=='x') { - fputs("fe_m4_write_hex(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned long)",e->out); emit_expr(e,arg); fputc(')',e->out); return; - } - if (verb=='c') { - fputs("fe_m4_write_char(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned char)",e->out); emit_expr(e,arg); fputc(')',e->out); return; - } - if (verb=='b') { - fputs("fe_m4_write_bool(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; - } - if (verb=='s' || (verb==' ' && t && t->kind==FE_TYPE_SLICE)) { - fputs("fe_m4_write_slice(",e->out); - emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; - } - if (error_value) { - fputs("fe_m4_write_error(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned long)",e->out); emit_expr(e,arg); fputs(".tag)",e->out); return; - } - if (verb==' ' && t && t->kind==FE_TYPE_BOOL) { - fputs("fe_m4_write_bool(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", ",e->out); emit_expr(e,arg); fputc(')',e->out); return; - } - if (verb==' ' && t && t->kind==FE_TYPE_CHAR) { - fputs("fe_m4_write_char(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (unsigned char)",e->out); emit_expr(e,arg); fputc(')',e->out); return; - } - fputs("fe_m4_write_int(",e->out); emit_m4_writer_value(e,writer,buffer); fputs(", (long)",e->out); emit_expr(e,arg); fputc(')',e->out); -} - -static void emit_m4_builtin(FeEmitter *e, FeNode *n) -{ - const char *fmt=n->aux_text; - FeNode *fmt_node=n->children; - FeNode *arg; - FeNode *writer_arg=0; - FeNode *buffer_arg=0; - unsigned long i,j,last=1; - unsigned count=0; - int verb; - int error_value; - int first=1; - int is_print=strcmp(n->text,"@print")==0; - int is_sprint=strcmp(n->text,"@sprint")==0; - if (!fmt) { fputs("0",e->out); return; } - if (is_print) writer_arg=0; - else if (is_sprint) { buffer_arg=fmt_node; fmt_node=fmt_node ? fmt_node->next : 0; } - else { writer_arg=fmt_node; fmt_node=fmt_node ? fmt_node->next : 0; } - arg=fmt_node ? fmt_node->next : 0; - error_value=0; - fputc('(',e->out); - if (!is_print && !is_sprint) { - fputs("fe_m4_error=0",e->out); - first=0; - } - if (is_sprint) { - fputs("fe_m4_sprint_begin((fe_m4_slice*)&",e->out); emit_expr(e,buffer_arg); fputs(")",e->out); - first=0; - } - i=1; - while(fmt[i] && fmt[i]!='"') { - if(fmt[i]=='\\') { ++i; if(fmt[i]) ++i; continue; } - if(fmt[i]=='{' && fmt[i+1]=='{') { i+=2; continue; } - if(fmt[i]=='}' && fmt[i+1]=='}') { i+=2; continue; } - if(fmt[i]=='{') { - j=i+1; while(fmt[j] && fmt[j]!='}') ++j; - if(!fmt[j]) break; - if(i>last) { - if(!first) fputs(", ",e->out); - if (!is_print && !is_sprint) - fputs("fe_m4_error ? fe_m4_error : (fe_m4_error = ",e->out); - if(is_print) { fputs("fe_m4_write_cstr(fe_m4_stdout_writer(), ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } - else { fputs("fe_m4_write_cstr(",e->out); if(is_sprint) emit_m4_writer_value(e,buffer_arg,1); else emit_m4_writer(e,writer_arg,0); fputs(", ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } - if (!is_print && !is_sprint) fputc(')',e->out); - first=0; - } - verb=(j==i+1) ? ' ' : (j==i+2 ? (unsigned char)fmt[i+1] : '?'); - if(arg) { - error_value=arg->sem_type && arg->sem_type->kind==FE_TYPE_ENUM && - arg->sem_type->is_error; - if(!first) fputs(", ",e->out); - if (!is_print && !is_sprint) - fputs("fe_m4_error ? fe_m4_error : (fe_m4_error = ",e->out); - if(is_print) emit_m4_arg(e,arg,verb,0,0,error_value); - else { - /* The writer expression is repeated intentionally; it is - a value wrapper and does not re-evaluate source args. */ - emit_m4_arg(e,arg,verb, is_sprint ? buffer_arg : writer_arg, - is_sprint,error_value); - } - if (!is_print && !is_sprint) fputc(')',e->out); - first=0; arg=arg->next; ++count; - } - last=j+1; i=j+1; continue; - } - ++i; - } - if(fmt[i]=='"' && i>last) { - if(!first) fputs(", ",e->out); - if (!is_print && !is_sprint) - fputs("fe_m4_error ? fe_m4_error : (fe_m4_error = ",e->out); - if(is_print) { fputs("fe_m4_write_cstr(fe_m4_stdout_writer(), ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } - else { fputs("fe_m4_write_cstr(",e->out); if(is_sprint) emit_m4_writer_value(e,buffer_arg,1); else emit_m4_writer(e,writer_arg,0); fputs(", ",e->out); emit_m4_piece(e->out,fmt,last,i); fputc(')',e->out); } - if (!is_print && !is_sprint) fputc(')',e->out); - first=0; - } - if(first) fputs("0",e->out); - if(is_print) fputs(", (void)0",e->out); - else if(is_sprint) { fputs(", fe_m4_sprint_finish()",e->out); } - fputc(')',e->out); - (void)count; -} - - -static void emit_destroy_expr(FeEmitter *e, FeNode *n) -{ - fputs("(free(",e->out); emit_expr(e,n); - if(n && n->sem_type && n->sem_type->kind==FE_TYPE_OWNED && - n->sem_type->elem && n->sem_type->elem->kind==FE_TYPE_SLICE) - fputs(".p",e->out); - fputs(")",e->out); - if (n && n->kind==FE_N_IDENT) { - fputs(", ",e->out); emit_lvalue(e,n); - if(n->sem_type && n->sem_type->elem && - n->sem_type->elem->kind==FE_TYPE_SLICE) fputs(".p=0",e->out); - else fputs("=0",e->out); - fputs(", fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs("=0",e->out); - } else { - fputs(", ",e->out); emit_lvalue(e,n); fputs("=0",e->out); - } - fputs(", 0)",e->out); -} - -static FeNode *init_field(FeNode *n, const char *name) -{ - FeNode *f; - for(f=n ? n->children : 0;f;f=f->next) - if(f->kind==FE_N_FIELD && f->text && name && strcmp(f->text,name)==0) return f; - return 0; -} - -static void emit_slice_call(FeEmitter *e, FeNode *n) -{ - FeType *bt=n->a ? n->a->sem_type : 0; - const char *maker=n->sem_type && n->sem_type->maker ? n->sem_type->maker : - (bt && bt->slicer ? bt->slicer : "fe_missing_slice"); - if (bt && (bt->kind==FE_TYPE_ARRAY || bt->kind==FE_TYPE_SLICE)) { - fputs(n->sem_type && n->sem_type->slicer ? - n->sem_type->slicer : "fe_missing_slicer",e->out); - fputc('(',e->out); fputs(maker,e->out); fputc('(',e->out); - if (bt->kind==FE_TYPE_ARRAY) { - fputs("(&",e->out); emit_lvalue(e,n->a); fputs(")->a",e->out); - } else { - emit_expr(e,n->a); fputs(".p",e->out); - } - fputs(", ",e->out); - if(bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); - else { emit_expr(e,n->a); fputs(".n",e->out); } - fputs("), ",e->out); - if(n->b) emit_expr(e,n->b); else fputs("0",e->out); - fputs(", ",e->out); - if(n->c) emit_expr(e,n->c); - else if(bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); - else { emit_expr(e,n->a); fputs(".n",e->out); } - fputc(')',e->out); - return; - } - if (!n->b && !n->c && bt && bt->full_slicer) { - fputs(bt->full_slicer,e->out); - if (bt->kind==FE_TYPE_ARRAY) { fputs("(&",e->out); emit_lvalue(e,n->a); } - else { fputc('(',e->out); emit_expr(e,n->a); } - fputc(')',e->out); return; - } - if (!n->c && bt && bt->tail_slicer) { - fputs(bt->tail_slicer,e->out); - if (bt->kind==FE_TYPE_ARRAY) { fputs("(&",e->out); emit_lvalue(e,n->a); } - else { fputc('(',e->out); emit_expr(e,n->a); } - fputs(", ",e->out); - if (n->b) emit_expr(e,n->b); else fputs("0",e->out); - fputc(')',e->out); return; - } - fputs(maker,e->out); - if (bt && bt->kind==FE_TYPE_ARRAY) { fputs("(&",e->out); emit_lvalue(e,n->a); } - else { fputc('(',e->out); emit_expr(e,n->a); } - fputs(", ",e->out); - if(n->b) emit_expr(e,n->b); else fputs("0",e->out); - fputs(", ",e->out); - if(n->c) emit_expr(e,n->c); - else if(bt && bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); - else fputs("((unsigned long)",e->out), emit_expr(e,n->a), fputs(".n)",e->out); - fputc(')',e->out); -} - -static void emit_slice(FeEmitter *e, FeNode *n) -{ - emit_slice_call(e,n); -} - -static void emit_expr_core(FeEmitter *e, FeNode *n) -{ - FeNode *x; - const char *op; - if (!n) { - fputs("0", e->out); - return; - } - switch (n->kind) { - case FE_N_IDENT: - if((n->flags & 0x100U) && n->sem_type && - type_needs_drop(n->sem_type)) { - fputs("(fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs("=0, ",e->out); fputs(cname(n,"fe_missing"),e->out); - fputc(')',e->out); - } else fputs(cname(n, "fe_missing"), e->out); - break; - case FE_N_LITERAL: - if (n->text && strcmp(n->text, "true") == 0) fputs("1", e->out); - else if (n->text && strcmp(n->text, "false") == 0) fputs("0", e->out); - else if (n->text && n->text[0]=='"') { - fputs(n->sem_type && n->sem_type->maker ? - n->sem_type->maker : "fe_missing_str",e->out); - fputs("((const unsigned char*)",e->out); - emit_c_literal(e->out,n->text,1); fputs(", sizeof(",e->out); - emit_c_literal(e->out,n->text,1); fputs(")-1)",e->out); - } - else if (n->text && n->text[0]=='\'') emit_c_literal(e->out,n->text,0); - else fputs(n->text ? n->text : "0", e->out); - break; - case FE_N_STRUCT_INIT: { - FeVariantType *v; - FeNode *f; - unsigned i; - if(n->sem_type && n->a && n->a->kind==FE_N_MEMBER) { - v=fe_type_variant(n->sem_type,n->a->b ? n->a->b->text : ""); - if(v) { fputs(v->maker,e->out); fputc('(',e->out); for(i=0;ifield_count;i++){f=init_field(n,v->fields[i].name);if(i)fputs(", ",e->out);if(f)emit_expr(e,f->a);else fputs("0",e->out);} fputc(')',e->out); } - else fputs("0",e->out); - } else if(n->sem_type && n->sem_type->maker) { - fputs(n->sem_type->maker,e->out); fputc('(',e->out); - if(n->sem_type->kind==FE_TYPE_STRUCT) { for(i=0;isem_type->field_count;i++){f=init_field(n,n->sem_type->fields[i].name);if(i)fputs(", ",e->out);if(f)emit_expr(e,f->a);else fputs("0",e->out);} } - fputc(')',e->out); - } else fputs("0",e->out); - break; - } - case FE_N_ARRAY_INIT: { - int first=1; - if(n->sem_type && n->sem_type->maker) { fputs(n->sem_type->maker,e->out); fputc('(',e->out); for(x=n->children;x;x=x->next){if(!first)fputs(", ",e->out);emit_expr(e,x);first=0;} fputc(')',e->out); } else fputs("0",e->out); - break; - } - case FE_N_INDEX: { - FeType *bt; - bt=n->a ? n->a->sem_type : 0; - if(n->c || !n->b) { - emit_slice(e,n); - } else { - if (bt && bt->indexer) { - fputs(bt->indexer,e->out); fputc('(',e->out); - emit_expr(e,n->a); fputs(", ",e->out); emit_expr(e,n->b); fputc(')',e->out); - } else fputs("0",e->out); - } - break; - } - case FE_N_UNARY: - op = n->text ? n->text : ""; - if (strcmp(op, "try") == 0) { - if (n->a && n->a->sem_type && - n->a->sem_type->kind==FE_TYPE_ERROR_UNION && - n->a->sem_type->error_value && - n->a->sem_type->error_value->kind!=FE_TYPE_VOID) { - fputc('(',e->out); emit_expr(e,n->a); fputs(").v",e->out); - } else emit_expr(e,n->a); - break; - } - if (strcmp(op, "not") == 0) fputs("(!", e->out); - else { - fputc('(', e->out); - fputs(strcmp(op,"&mut")==0 ? "&" : op, e->out); - } - /* Borrowing needs a place, not a value. emit_expr lowers an index to - the bounds-checking accessor, and the address of that call is not an - lvalue -- `&s[0]` became `&fe_idx_slice_type_2(s, 0)`, which C - rejects. emit_lvalue spells the same element as `s.p[0]`. */ - if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) emit_lvalue(e, n->a); - else emit_expr(e, n->a); - fputc(')', e->out); - break; - case FE_N_BINARY: - op = n->text ? n->text : "+"; - fputc('(', e->out); - emit_expr(e, n->a); - if (strcmp(op, "and") == 0) fputs(" && ", e->out); - else if (strcmp(op, "or") == 0) fputs(" || ", e->out); - else fputs(op, e->out); - emit_expr(e, n->b); - fputc(')', e->out); - break; - case FE_N_TYPE: - if (n->text && strcmp(n->text, "as") == 0) { - fputs("((", e->out); - fputs(ctype(e, n->b), e->out); - fputc(')', e->out); - emit_expr(e, n->a); - fputc(')', e->out); - } else emit_expr(e, n->a); - break; - case FE_N_CALL: { - FeVariantType *v; - FeNode *call_param=0; - int special=0; - if(n->text && (strcmp(n->text,"@print")==0 || strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { emit_m4_builtin(e,n); special=1; } - else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && - n->a->b->text && strcmp(n->a->b->text,"destroy")==0 && - n->children) { - emit_destroy_expr(e,n->children); - special=1; - } - else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && - n->a->b->text && strcmp(n->a->b->text,"create")==0 && - n->children) { - FeType *created=n->children->sem_type; - FeType *owned=fe_type_owned(&e->check->types,created); - FeType *result=fe_type_error_union(&e->check->types,owned); - if (result->alloc_cname) fputs(result->alloc_cname,e->out); - else fputs("fe_bad_alloc",e->out); - fputc('(',e->out); emit_expr(e,n->children); fputc(')',e->out); - special=1; - } - else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && - n->a->b->text && strcmp(n->a->b->text,"alloc_slice")==0 && - n->children && n->children->next) { - FeType *result=n->sem_type; - if(result && result->alloc_cname) fputs(result->alloc_cname,e->out); - else fputs("fe_bad_slice_alloc",e->out); - fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out); - special=1; - } - else if(n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"trim")==0 && !n->children && - n->a->a && n->a->a->sem_type && - n->a->a->sem_type->kind==FE_TYPE_SLICE && - n->a->a->sem_type->cname) { - fputs("fe_trim_",e->out); fputs(n->a->a->sem_type->cname,e->out); - fputc('(',e->out); emit_expr(e,n->a->a); fputc(')',e->out); - special=1; - } - else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && - n->a->b->text && strcmp(n->a->b->text,"replace")==0 && - n->children && n->children->next && n->sem_type) { - fputs(n->sem_type->replace_cname ? n->sem_type->replace_cname : - "fe_bad_replace",e->out); - fputc('(',e->out); emit_expr(e,n->children); fputs(", ",e->out); - emit_expr(e,n->children->next); fputc(')',e->out); - special=1; - } - else if(n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"null_writer")==0) { fputs("fe_m4_null_writer()",e->out); special=1; } - else if(n->a && n->a->kind==FE_N_MEMBER && n->sem_decl && - n->sem_decl->kind==FE_N_FN) { - FeNode *mp=n->sem_decl->a ? n->sem_decl->a->children : 0; - FeNode *ma; - fputs(cname(n->sem_decl,"fe_method"),e->out); fputc('(',e->out); - if(mp && mp->sem_type && mp->sem_type->kind==FE_TYPE_REF) { - fputc('&',e->out); emit_lvalue(e,n->a->a); - } else emit_expr(e,n->a->a); - for(ma=n->children; ma; ma=ma->next) { - fputs(", ",e->out); emit_expr(e,ma); - } - fputc(')',e->out); - special=1; - } - else if(!n->a && n->text && strcmp(n->text,"@size_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%lu",fe_type_size(fe_type_intern(&e->check->types,n->children->text))); special=1; } - else if(!n->a && n->text && strcmp(n->text,"@align_of")==0 && n->children && n->children->kind==FE_N_IDENT) { fprintf(e->out,"%u",fe_type_align(fe_type_intern(&e->check->types,n->children->text))); special=1; } - else if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM) { - v=fe_type_variant(n->a->a->sem_type,n->a->b ? n->a->b->text : ""); - if(v) fputs(v->maker,e->out); else fputs("fe_bad_variant",e->out); - } else if (n->a) emit_expr(e, n->a); - else fputs(n->text ? n->text : "fe_builtin", e->out); - if(!special) { - if(n->sem_decl && n->sem_decl->kind==FE_N_FN && n->sem_decl->a) - call_param=n->sem_decl->a->children; - fputc('(', e->out); - for (x = n->children; x; x = x->next) { - FeType *want=call_param && call_param->a ? - fe_type_from_ast(&e->check->types,call_param->a) : 0; - if (x != n->children) fputs(", ", e->out); - if(want && want->kind==FE_TYPE_SLICE && !want->ref_mut && - x->sem_type && x->sem_type->kind==FE_TYPE_SLICE && - x->sem_type->ref_mut) { - fputs(want->maker,e->out); fputc('(',e->out); - emit_expr(e,x); fputs(".p, ",e->out); - emit_expr(e,x); fputs(".n)",e->out); - } else if ((x->flags & 0x100U) && x->kind==FE_N_IDENT && - x->sem_type && x->sem_type->kind==FE_TYPE_OWNED) { - fputs("(fe_live_",e->out); fputs(cname(x,"owned"),e->out); - fputs("=0, ",e->out); emit_expr(e,x); fputc(')',e->out); - } else emit_expr(e, x); - if(call_param) call_param=call_param->next; - } - fputc(')', e->out); - } - break; - } - case FE_N_MEMBER: { - FeVariantType *v; - if(n->a && n->a->kind==FE_N_IDENT && n->a->text && - strcmp(n->a->text,"io")==0 && n->b && n->b->text && - (strcmp(n->b->text,"stdout")==0 || strcmp(n->b->text,"stderr")==0)) - fputs(strcmp(n->b->text,"stderr")==0 ? - "fe_m4_stderr_writer()" : "fe_m4_stdout_writer()",e->out); - else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); - } else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OWNED && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputs(")",e->out); - } else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_ENUM) { v=fe_type_variant(n->a->sem_type,n->b ? n->b->text : ""); if(v) fputs(v->maker,e->out); else fputs("0",e->out); if(v)fputs("()",e->out); } - else if(n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_REF) { - emit_expr(e,n->a); fputs("->",e->out); - if(n->b) fputs(n->b->text ? n->b->text : "member",e->out); - } else { emit_expr(e, n->a); fputc('.', e->out); if (n->b) fputs(n->b->text ? n->b->text : "member", e->out); } - break; - } - default: - fputs("0", e->out); - break; - } -} - - - - - - - - - - -static void emit_match(FeEmitter *e, FeNode *n, int value_context) -{ - FeNode *arm; - FeType *t=n->a ? n->a->sem_type : 0; - FeVariantType *v; - FeNode *b; - unsigned i; - char temp[32]; - sprintf(temp,"fe_match_%u",e->temp_serial++); - pad(e); fputs("{\n",e->out); ++e->indent; - pad(e); fputs(fe_type_c_name(t,e->pointer_bits),e->out); fputc(' ',e->out); - fputs(temp,e->out); fputs(" = ",e->out); emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("switch (",e->out); fputs(temp,e->out); fputs(".tag) {\n",e->out); ++e->indent; - for(arm=n->children;arm;arm=arm->next) { - if(arm->text && strcmp(arm->text,"_")==0) { pad(e); fputs("default: ",e->out); } - else { v=t && t->kind==FE_TYPE_ENUM ? fe_type_variant(t,arm->text) : 0; if(!v) continue; fprintf(e->out,"case %u: ",v->tag); } - fputs("{\n",e->out); ++e->indent; - v=t && t->kind==FE_TYPE_ENUM ? fe_type_variant(t,arm->text) : 0; - if(v) for(i=0,b=arm->children;ifield_count && b;i++,b=b->next) { - pad(e); fputs(fe_type_c_name(v->fields[i].type,e->pointer_bits),e->out); fputc(' ',e->out); fputs(cname(b,"fe_match"),e->out); fputs(" = ",e->out); fputs(temp,e->out); fputs(".payload.",e->out); fputs(v->name,e->out); if(v->field_count>1){fputc('.',e->out);fputs(v->fields[i].name,e->out);} fputs(";\n",e->out); - } - if(arm->a && arm->a->kind==FE_N_BLOCK) emit_stmt(e,arm->a); else { pad(e); emit_expr(e,arm->a); fputs(";\n",e->out); } - pad(e); fputs("break;\n",e->out); --e->indent; pad(e); fputs("}\n",e->out); - } - --e->indent; pad(e); fputs("}\n",e->out); - --e->indent; pad(e); fputs("}\n",e->out); - if (value_context || match_definitely_returns(n)) { - pad(e); fputs("fe_trap_bounds();\n",e->out); - pad(e); fputs("return 0;\n",e->out); - } -} - -static int try_has_value(FeNode *n) -{ - return n && n->kind==FE_N_UNARY && n->text && strcmp(n->text,"try")==0 && - n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_ERROR_UNION && - n->a->sem_type->error_value && n->a->sem_type->error_value->kind!=FE_TYPE_VOID; -} - -static void emit_try_statement(FeEmitter *e, FeNode *try_node, FeNode *target) -{ - char temp[40]; - char error[48]; - FeType *result=try_node->a->sem_type; - sprintf(temp,"fe_try_%u",e->temp_serial++); - sprintf(error,"%s.e",temp); - pad(e); fputs("{ ",e->out); fputs(fe_type_c_name(result,e->pointer_bits),e->out); - fputc(' ',e->out); fputs(temp,e->out); fputs(" = ",e->out); - emit_expr(e,try_node->a); fputs("; if (",e->out); fputs(temp,e->out); - fputs(".e) {\n",e->out); ++e->indent; - emit_cleanup_all(e); - pad(e); emit_error_return(e,error); - --e->indent; pad(e); fputs("} ",e->out); - if (target) { - if (target->kind==FE_N_LET || target->kind==FE_N_VAR) - fputs(cname(target,"fe_local"),e->out); - else - emit_lvalue(e,target); - fputs(" = ",e->out); fputs(temp,e->out); fputs(".v;\n",e->out); - } - fputs("}\n",e->out); -} - -static void emit_stmt_core(FeEmitter *e, FeNode *n) -{ - if (!n) return; - switch (n->kind) { - case FE_N_BLOCK: - emit_block(e, n); - fputc('\n', e->out); - break; - case FE_N_LET: - case FE_N_VAR: - if (n->b) { - if (try_has_value(n->b)) emit_try_statement(e,n->b,n); - else { - pad(e); - fputs(cname(n, "fe_local"), e->out); - fputs(" = ", e->out); - emit_expr(e, n->b); - fputs(";\n", e->out); - } - emit_owned_live(e,n,1); - } - break; - case FE_N_ASSIGN: - if (n->a && n->a->kind==FE_N_IDENT && n->a->sem_type && - n->a->sem_type->kind==FE_TYPE_OWNED) { - pad(e); fputs("if (fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); - fputs(") { ",e->out); - if(n->a->sem_type->elem && - n->a->sem_type->elem->kind==FE_TYPE_SLICE) { - fputs("free(",e->out); fputs(cname(n->a,"owned"),e->out); - fputs(".p); ",e->out); - } else if (n->a->sem_type->elem && type_needs_drop(n->a->sem_type->elem) && - n->a->sem_type->elem->drop_cname) - fprintf(e->out,"%s(%s); ",n->a->sem_type->elem->drop_cname,cname(n->a,"owned")); - if(!(n->a->sem_type->elem && - n->a->sem_type->elem->kind==FE_TYPE_SLICE)) { - fputs("free(",e->out); fputs(cname(n->a,"owned"),e->out); fputs("); ",e->out); - } - fputs("fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); - fputs("=0; }\n",e->out); - } else if(n->a && n->a->kind==FE_N_IDENT && n->a->sem_type && - type_needs_drop(n->a->sem_type) && - n->a->sem_type->drop_cname) { - pad(e); fputs("if (fe_live_",e->out); fputs(cname(n->a,"local"),e->out); - fprintf(e->out,") { %s(&%s); fe_live_", - n->a->sem_type->drop_cname,cname(n->a,"local")); - fputs(cname(n->a,"local"),e->out); fputs("=0; }\n",e->out); - } - pad(e); - emit_lvalue(e, n->a); - fputc(' ', e->out); - fputs(n->text ? n->text : "=", e->out); - fputs(" ", e->out); - if (n->b && (n->b->flags & 0x100U) && n->b->kind==FE_N_IDENT && - n->b->sem_type && n->b->sem_type->kind==FE_TYPE_OWNED) { - fputs("(fe_live_",e->out); fputs(cname(n->b,"owned"),e->out); - fputs("=0, ",e->out); emit_expr(e,n->b); fputc(')',e->out); - } else emit_expr(e, n->b); - fputs(";\n", e->out); - if (n->a && n->a->kind==FE_N_IDENT) emit_owned_live(e,n->a,1); - break; - case FE_N_EXPR_STMT: - if (try_has_value(n->a)) { - emit_try_statement(e,n->a,0); - } else { - pad(e); - if (n->a && n->a->kind==FE_N_UNARY && n->a->text && - strcmp(n->a->text,"try")==0 && n->a->a) { - fputs("if ((fe_error_temp = ",e->out); - emit_expr(e,n->a->a); - fputs(") != 0) {\n",e->out); - ++e->indent; - emit_cleanup_all(e); - pad(e); fputs("return fe_error_temp;\n",e->out); - --e->indent; - pad(e); fputs("}\n",e->out); - } else { - emit_expr(e, n->a); - fputs(";\n", e->out); - } - } - break; - case FE_N_BREAK: - case FE_N_CONTINUE: - if (e->loop_depth) { - emit_cleanup_to(e,e->loop_floor[e->loop_depth-1]); - pad(e); fputs(n->kind==FE_N_BREAK ? "break;\n" : "continue;\n",e->out); - } - break; - case FE_N_RETURN: - if (n->a && n->a->kind == FE_N_MATCH) { - emit_match(e,n->a,1); - break; - } - /* Evaluate before cleanup: `return p.^` must not dereference p after - its owned cleanup has run. The block-local temporary is declared - before all statements to retain C89 declaration ordering. */ - if (n->a && e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs("fe_return_value = ",e->out); - if ((n->a->flags & 0x100U) && n->a->kind==FE_N_IDENT && - n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OWNED) { - fputs("(fe_live_",e->out); fputs(cname(n->a,"owned"),e->out); - fputs("=0, ",e->out); emit_expr(e,n->a); fputc(')',e->out); - } else emit_expr(e,n->a); - fputs(";\n",e->out); - } - emit_cleanup_all(e); - pad(e); fputs("return",e->out); - if (n->a) fputs(" fe_return_value",e->out); - fputs(";\n",e->out); - break; - case FE_N_IF: - pad(e); - fputs("if (", e->out); - emit_expr(e, n->a); - fputs(") ", e->out); - if (n->b && n->b->kind == FE_N_BLOCK) emit_block(e, n->b); - else emit_block(e, 0); - if (n->c) { - fputs(" else ", e->out); - if (n->c->kind == FE_N_IF) emit_stmt(e, n->c); - else emit_block(e, n->c); - } - fputc('\n', e->out); - break; - case FE_N_WHILE: - pad(e); - fputs("while (", e->out); - emit_expr(e, n->a); - fputs(") ", e->out); - if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; - if (n->b && n->b->kind == FE_N_BLOCK) emit_block(e, n->b); - else emit_block(e, 0); - if (e->loop_depth) --e->loop_depth; - fputc('\n', e->out); - break; - case FE_N_FOR: - pad(e); fputs("{\n",e->out); ++e->indent; - if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; - if (n->c) { - pad(e); fputs("unsigned long ",e->out); fputs(cname(n,"fe_index"),e->out); fputs(";\n",e->out); - pad(e); fputs(cname(n,"fe_index"),e->out); fputs(" = ",e->out); emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("for (; ",e->out); fputs(cname(n,"fe_index"),e->out); fputs(" < ",e->out); emit_expr(e,n->c); fputs("; ++",e->out); fputs(cname(n,"fe_index"),e->out); fputs(") ",e->out); emit_block(e,n->b); fputc('\n',e->out); - } else { - FeType *bt=n->a ? n->a->sem_type : 0; - FeType *et=bt ? bt->elem : 0; - char temp[32]; - int mutable_iter=(n->flags & 4U) != 0; - sprintf(temp,"fe_iter_%u",e->temp_serial++); - pad(e); fputs(fe_type_c_name(bt,e->pointer_bits),e->out); if (mutable_iter) fputs(" *",e->out); fputc(' ',e->out); fputs(temp,e->out); fputs(" = ",e->out); if (mutable_iter) fputc('&',e->out); emit_expr(e,n->a); fputs(";\n",e->out); - if (n->aux_text) { - pad(e); fputs("unsigned long ",e->out); fputs(cname(n,"fe_index"),e->out); fputs(";\n",e->out); - } else { - pad(e); fputs(fe_type_c_name(n->sem_type ? n->sem_type : fe_type_ref(&e->check->types,et,0),e->pointer_bits),e->out); fputc(' ',e->out); fputs(cname(n,"fe_item"),e->out); fputs(";\n",e->out); - } - pad(e); fputs("{ unsigned long fe_i; for (fe_i = 0; fe_i < ",e->out); - if(bt && bt->kind==FE_TYPE_ARRAY) fprintf(e->out,"%lu",bt->length); else { if(mutable_iter) fputs("(*",e->out); fputs(temp,e->out); if(mutable_iter) fputs(").n",e->out); else fputs(".n",e->out); } - fputs("; ++fe_i) { ",e->out); - if(n->aux_text) { - fputs(fe_type_c_name(fe_type_ref(&e->check->types,et,(n->flags & 4U) != 0),e->pointer_bits),e->out); fputc(' ',e->out); fputs(n->aux_cname ? n->aux_cname : "fe_item",e->out); fputs("; ",e->out); - fputs(cname(n,"fe_index"),e->out); fputs(" = fe_i; ",e->out); - fputs(n->aux_cname ? n->aux_cname : "fe_item",e->out); fputs(" = ",e->out); - } else { fputs(cname(n,"fe_item"),e->out); fputs(" = ",e->out); } - fputc('&',e->out); if(mutable_iter) fputs("(*",e->out); fputs(temp,e->out); if(mutable_iter) fputs(")",e->out); if(bt && bt->kind==FE_TYPE_ARRAY) fputs(".a[fe_i]",e->out); else fputs(".p[fe_i]",e->out); fputs("; ",e->out); - emit_block(e,n->b); fputs(" } }\n",e->out); - } - --e->indent; - if (e->loop_depth) --e->loop_depth; - pad(e); fputs("}\n",e->out); break; - case FE_N_MATCH: - emit_match(e,n,0); break; - default: - break; - } -} - - -static void emit_main_wrapper_core(FeEmitter *e, FeNode *fn) -{ - fputs("int main(void) {\n ", e->out); - if (fn->sem_type && fn->sem_type->kind == FE_TYPE_VOID) { - fputs(cname(fn, "fe_main"), e->out); - fputs("();\n return 0;\n", e->out); - } else { - fputs("return ", e->out); - fputs(cname(fn, "fe_main"), e->out); - fputs("();\n", e->out); - } - fputs("}\n", e->out); -} - -void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, - unsigned pointer_bits, int no_checks) -{ - e->out = out; - e->check = check; - e->pointer_bits = pointer_bits; - e->indent = 0; - e->no_checks = no_checks; - e->temp_serial = 0; - e->fallthrough_block = 0; - e->block_depth = 0; - e->loop_depth = 0; - e->current_ret = 0; - e->current_fn = 0; -} - - -#include "m7.h" -#include "lower.h" - -static void emit_expr(FeEmitter *e, FeNode *n); -static void emit_stmt(FeEmitter *e, FeNode *n); -static void emit_block(FeEmitter *e, FeNode *n); - -static int type_needs_drop(FeType *t) -{ - return fe_lower_type_needs_drop(t); -} - -static const char *m7_c_type(FeEmitter *e, FeType *t) -{ - if (!t) return "long"; - if ((t->kind==FE_TYPE_ENUM && t->is_error) || - strcmp(t->name,"core.Error")==0) - return "unsigned short"; - return fe_type_c_name(t,e->pointer_bits); -} - -static char *m7_temp_name(FeEmitter *e) -{ - char number[24]; - char *p; - unsigned long len; - sprintf(number,"%u",e->temp_serial++); - len=(unsigned long)strlen("fe_m7_tmp_")+ - (unsigned long)strlen(number)+1UL; - p=(char *)fe_arena_alloc(&e->check->ast->arena,len); - if (!p) return 0; - strcpy(p,"fe_m7_tmp_"); - strcat(p,number); - return p; -} - -static int m7_needs_temp(FeNode *n) -{ - if (!n) return 0; - if (fe_m7_is_try(n)) return 1; - if (n->kind==FE_N_BINARY && fe_m7_lazy_kind(n)!=FE_M7_LAZY_NONE) - return 1; - if (n->kind==FE_N_IF && n->text && strcmp(n->text,"if let")==0) - return 1; - if (n->kind==FE_N_MATCH && n->a && n->a->sem_type && - n->a->sem_type->kind==FE_TYPE_OPTIONAL) - return 1; - return 0; -} - -static FeType *m7_temp_type(FeNode *n) -{ - if (!n) return 0; - if (fe_m7_is_try(n)) return n->a ? n->a->sem_type : 0; - if (n->kind==FE_N_BINARY) return n->a ? n->a->sem_type : 0; - if ((n->kind==FE_N_IF || n->kind==FE_N_MATCH) && n->a) - return n->a->sem_type; - return 0; -} - -static void m7_prepare_temps(FeEmitter *e, FeNode *n) -{ - FeNode *x; - if (!n) return; - if (m7_needs_temp(n) && !n->aux_cname) - n->aux_cname=m7_temp_name(e); - m7_prepare_temps(e,n->a); - m7_prepare_temps(e,n->b); - m7_prepare_temps(e,n->c); - for (x=n->children;x;x=x->next) m7_prepare_temps(e,x); -} - -static void m7_emit_temp_decls(FeEmitter *e, FeNode *n) -{ - FeNode *x; - FeType *t; - if (!n) return; - if (m7_needs_temp(n) && n->aux_cname) { - t=m7_temp_type(n); - if (t) { - pad(e); fputs(m7_c_type(e,t),e->out); fputc(' ',e->out); - fputs(n->aux_cname,e->out); fputs(";\n",e->out); - } - } - m7_emit_temp_decls(e,n->a); - m7_emit_temp_decls(e,n->b); - m7_emit_temp_decls(e,n->c); - for (x=n->children;x;x=x->next) m7_emit_temp_decls(e,x); -} - -static void m7_emit_type(FeEmitter *e, FeType *t) -{ - unsigned i; - unsigned j; - if (!t || t->emit_state) return; - if (t->kind==FE_TYPE_OPTIONAL) { - t->emit_state=1; - m7_emit_type(e,t->elem); - if (!fe_m7_optional_uses_niche(t->elem) && t->cname) { - fputs(t->cname,e->out); fputs(" { unsigned char has; ",e->out); - fputs(m7_c_type(e,t->elem),e->out); - fputs(" v; };\n",e->out); - } - t->emit_state=2; - return; - } - if (t->kind==FE_TYPE_ARRAY) m7_emit_type(e,t->elem); - if (t->kind==FE_TYPE_SLICE) m7_emit_type(e,t->elem); - if (t->kind==FE_TYPE_OWNED) m7_emit_type(e,t->elem); - if (t->kind==FE_TYPE_STRUCT) - for (i=0;ifield_count;++i) m7_emit_type(e,t->fields[i].type); - if (t->kind==FE_TYPE_ENUM) - for (i=0;ivariant_count;++i) - for (j=0;jvariants[i].field_count;++j) - m7_emit_type(e,t->variants[i].fields[j].type); - if (t->kind==FE_TYPE_ERROR_UNION) { - m7_emit_type(e,t->elem); - m7_emit_type(e,t->error_value); - } - emit_one_type(e,t); -} - -static void emit_type_defs(FeEmitter *e) -{ - FeType *t; - for (t=e->check->types.types;t;t=t->next) - if (t->kind==FE_TYPE_ARRAY) - fe_type_slice(&e->check->types,t->elem); - for (t=e->check->types.types;t;t=t->next) m7_emit_type(e,t); -} - -static void m7_emit_drop_access(FeEmitter *e, FeType *t, - const char *access) -{ - if (!t || !access || !type_needs_drop(t)) return; - if (t->kind==FE_TYPE_OWNED) { - if (t->elem && t->elem->kind==FE_TYPE_SLICE) { - fprintf(e->out,"if ((%s).p) { free((%s).p); (%s).p=0; } ", - access,access,access); - } else { - fprintf(e->out,"if (%s) { ",access); - if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) - fprintf(e->out,"%s(%s); ",t->elem->drop_cname,access); - fprintf(e->out,"free(%s); %s=0; } ",access,access); - } - return; - } - if (t->drop_cname) - fprintf(e->out,"%s(&(%s)); ",t->drop_cname,access); -} - -static void m7_emit_drop_helpers(FeEmitter *e) -{ - FeType *t; - FeNode *method; - unsigned i; - char access[256]; - for (t=e->check->types.types;t;t=t->next) - if (type_needs_drop(t) && t->drop_cname && - (t->kind==FE_TYPE_STRUCT || t->kind==FE_TYPE_ARRAY || - t->kind==FE_TYPE_OPTIONAL || t->kind==FE_TYPE_ERROR_UNION)) - fprintf(e->out,"static void %s(%s *self);\n", - t->drop_cname,m7_c_type(e,t)); - for (t=e->check->types.types;t;t=t->next) { - if (!type_needs_drop(t) || !t->drop_cname) continue; - if (t->kind==FE_TYPE_STRUCT) { - fprintf(e->out,"static void %s(%s *self) { ", - t->drop_cname,m7_c_type(e,t)); - method=find_drop_method(e,t->name); - if (method) fprintf(e->out,"%s(self); ",cname(method,"fe_drop_method")); - for (i=t->field_count;i>0;--i) { - sprintf(access,"self->%s",t->fields[i-1U].name); - m7_emit_drop_access(e,t->fields[i-1U].type,access); - } - fputs("}\n",e->out); - } else if (t->kind==FE_TYPE_ARRAY) { - fprintf(e->out,"static void %s(%s *self) { unsigned long i; for (i=0; i<%lu; ++i) { ", - t->drop_cname,m7_c_type(e,t),t->length); - strcpy(access,"self->a[i]"); - m7_emit_drop_access(e,t->elem,access); - fputs("} }\n",e->out); - } else if (t->kind==FE_TYPE_OPTIONAL) { - fprintf(e->out,"static void %s(%s *self) { ", - t->drop_cname,m7_c_type(e,t)); - if (fe_m7_optional_uses_niche(t->elem)) { - fputs("if (*self) { ",e->out); - m7_emit_drop_access(e,t->elem,"*self"); - fputs("} ",e->out); - } else { - fputs("if (self->has) { ",e->out); - m7_emit_drop_access(e,t->elem,"self->v"); - fputs("self->has=0; } ",e->out); - } - fputs("}\n",e->out); - } else if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && - t->error_value->kind!=FE_TYPE_VOID) { - fprintf(e->out,"static void %s(%s *self) { if (!self->e) { ", - t->drop_cname,m7_c_type(e,t)); - m7_emit_drop_access(e,t->error_value,"self->v"); - fputs("self->e=1; } }\n",e->out); - } - } - /* Error enums are scalar codes, so their enum payload helper functions - from M3 are deliberately not emitted in the M7 path. */ -} - -static void m7_emit_type_helpers(FeEmitter *e) -{ - FeType *t; - FeType *st; - unsigned i; - unsigned j; - const char *ct; - FeVariantType *v; - for (t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_OPTIONAL) { - if (!fe_m7_optional_uses_niche(t->elem)) { - fprintf(e->out,"static %s %s(%s v) { %s r; r.has=1; r.v=v; return r; }\n", - m7_c_type(e,t),t->maker,m7_c_type(e,t->elem),m7_c_type(e,t)); - fprintf(e->out,"static %s %s(void) { %s r; memset(&r,0,sizeof(r)); return r; }\n", - m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); - fprintf(e->out,"static %s %s(%s x) { ", - m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); - if (!e->no_checks) fputs("if (!x.has) fe_trap_bounds(); ",e->out); - fputs("return x.v; }\n",e->out); - } else { - fprintf(e->out,"static %s %s(%s x) { ", - m7_c_type(e,t->elem),t->unwrap_cname,m7_c_type(e,t)); - if (!e->no_checks) fputs("if (!x) fe_trap_bounds(); ",e->out); - fputs("return x; }\n",e->out); - } - } - if (t->kind==FE_TYPE_ERROR_UNION && t->error_value && - t->error_value->kind!=FE_TYPE_VOID) { - fprintf(e->out,"static %s %s(unsigned short e, %s v) { %s r; r.e=e; r.v=v; return r; }\n", - m7_c_type(e,t),t->maker,m7_c_type(e,t->error_value),m7_c_type(e,t)); - if (t->none_cname) - fprintf(e->out,"static %s %s(unsigned short e) { %s r; memset(&r,0,sizeof(r)); r.e=e; return r; }\n", - m7_c_type(e,t),t->none_cname,m7_c_type(e,t)); - if (t->error_value->kind==FE_TYPE_OWNED && - t->error_value->elem && t->error_value->elem->kind==FE_TYPE_SLICE) { - FeType *item=t->error_value->elem->elem; - fprintf(e->out,"static %s %s(unsigned long n) { %s r; r.v.p=(%s*)malloc(sizeof(%s)*n); r.v.n=n; r.e=(r.v.p || !n) ? 0 : 1; return r; }\n", - m7_c_type(e,t),t->alloc_cname,m7_c_type(e,t), - m7_c_type(e,item),m7_c_type(e,item)); - } else if (t->error_value->kind==FE_TYPE_OWNED) { - fprintf(e->out,"static %s %s(%s v) { %s r; r.v=(%s)malloc(sizeof(%s)); if(r.v) *r.v=v; r.e=r.v ? 0 : 1; return r; }\n", - m7_c_type(e,t),t->alloc_cname, - m7_c_type(e,t->error_value->elem),m7_c_type(e,t), - m7_c_type(e,t->error_value), - m7_c_type(e,t->error_value->elem)); - } - } - } - for (t=e->check->types.types;t;t=t->next) { - if (t->replace_cname) { - ct=m7_c_type(e,t); - fprintf(e->out,"static %s %s(%s *dst, %s value) { %s old=*dst; *dst=value; return old; }\n", - ct,t->replace_cname,ct,ct,ct); - } - if (t->kind==FE_TYPE_STRUCT && t->maker) { - fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); - for (i=0;ifield_count;i++) { - if (i) fputs(", ",e->out); - fputs(m7_c_type(e,t->fields[i].type),e->out); - fprintf(e->out," p%u",i); - } - fprintf(e->out,") { %s v;",m7_c_type(e,t)); - for (i=0;ifield_count;i++) - fprintf(e->out," v.%s=p%u;",t->fields[i].name,i); - fputs(" return v; }\n",e->out); - } else if (t->kind==FE_TYPE_ARRAY && t->maker) { - fprintf(e->out,"static %s %s(",m7_c_type(e,t),t->maker); - for (i=0;ilength;i++) { - if (i) fputs(", ",e->out); - fputs(m7_c_type(e,t->elem),e->out); - fprintf(e->out," p%u",i); - } - fprintf(e->out,") { %s v;",m7_c_type(e,t)); - for (i=0;ilength;i++) fprintf(e->out," v.a[%u]=p%u;",i,i); - fputs(" return v; }\n",e->out); - } else if (t->kind==FE_TYPE_ENUM && !t->is_error) { - for (i=0;ivariant_count;i++) { - v=&t->variants[i]; - fprintf(e->out,"static %s %s(",m7_c_type(e,t),v->maker); - for (j=0;jfield_count;j++) { - if (j) fputs(", ",e->out); - fputs(m7_c_type(e,v->fields[j].type),e->out); - fprintf(e->out," p%u",j); - } - fprintf(e->out,") { %s x; x.tag=%u;",m7_c_type(e,t),v->tag); - for (j=0;jfield_count;j++) { - if (v->field_count==1) - fprintf(e->out," x.payload.%s=p%u;",v->name,j); - else - fprintf(e->out," x.payload.%s.%s=p%u;",v->name, - v->fields[j].name,j); - } - fputs(" return x; }\n",e->out); - } - } - } - m7_emit_drop_helpers(e); - /* Reuse the mature M3 index/slice helper generator. It does not depend - on M7 drop policy and all wrapper dependencies are already emitted. */ - for (t=e->check->types.types;t;t=t->next) { - if (t->kind==FE_TYPE_ARRAY && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); - if (!e->no_checks) - fprintf(e->out,"if (i >= %lu) fe_trap_bounds(); ",t->length); - fputs("return x.a[i]; }\n",e->out); - if (t->slicer) { - st=fe_type_slice(&e->check->types,t->elem); - fprintf(e->out,"static %s %s(%s *x, unsigned long a, unsigned long b) { ", - m7_c_type(e,st),t->slicer,m7_c_type(e,t)); - if (!e->no_checks) - fprintf(e->out,"if (a > b || b > %lu) fe_trap_bounds(); ",t->length); - fprintf(e->out,"return %s(x->a+a,b-a); }\n",st->maker); - fprintf(e->out,"static %s %s(%s *x) { return %s(x,0,%lu); }\n", - m7_c_type(e,st),t->full_slicer,m7_c_type(e,t),t->slicer,t->length); - fprintf(e->out,"static %s %s(%s *x, unsigned long a) { return %s(x,a,%lu); }\n", - m7_c_type(e,st),t->tail_slicer,m7_c_type(e,t),t->slicer,t->length); - } - } else if (t->kind==FE_TYPE_SLICE && t->indexer) { - fprintf(e->out,"static %s %s(%s x, unsigned long i) { ", - m7_c_type(e,t->elem),t->indexer,m7_c_type(e,t)); - if (!e->no_checks) fputs("if (i >= x.n) fe_trap_bounds(); ",e->out); - fputs("return x.p[i]; }\n",e->out); - if (t->slicer) { - fprintf(e->out,"static %s %s(%s x, unsigned long a, unsigned long b) { ", - m7_c_type(e,t),t->slicer,m7_c_type(e,t)); - if (!e->no_checks) - fputs("if (a > b || b > x.n) fe_trap_bounds(); ",e->out); - fprintf(e->out,"return %s(x.p+a,b-a); }\n",t->maker); - fprintf(e->out,"static %s %s(%s x) { return %s(x,0,x.n); }\n", - m7_c_type(e,t),t->full_slicer,m7_c_type(e,t),t->slicer); - fprintf(e->out,"static %s %s(%s x, unsigned long a) { return %s(x,a,x.n); }\n", - m7_c_type(e,t),t->tail_slicer,m7_c_type(e,t),t->slicer); - if (node_uses_trim(e->check->ast->root) && !t->ref_mut) { - fprintf(e->out, - "static %s fe_trim_%s(%s s) { unsigned long a=0; unsigned long b=s.n;" - " while (aa && (s.p[b-1]==' '||s.p[b-1]=='\\t'||s.p[b-1]=='\\r'||s.p[b-1]=='\\n')) --b;" - " return %s(s.p+a,b-a); }\n", - t->cname,t->cname,t->cname,t->maker); - } - } - } - } -} - -static void m7_emit_present(FeEmitter *e, FeType *opt, const char *name) -{ - if (fe_m7_optional_uses_niche(opt->elem)) { - fputs("(",e->out); fputs(name,e->out); fputs(" != 0)",e->out); - } else { - fputs(name,e->out); fputs(".has",e->out); - } -} - -static void m7_emit_payload_var(FeEmitter *e, FeType *opt, const char *name) -{ - (void)e; - fputs(name,e->out); - if (!fe_m7_optional_uses_niche(opt->elem)) fputs(".v",e->out); -} - -static void m7_emit_error_member(FeEmitter *e, FeNode *n) -{ - FeVariantType *v; - FeType *t; - t=n && n->a ? n->a->sem_type : 0; - v=t && t->kind==FE_TYPE_ENUM ? - fe_type_variant(t,n->b ? n->b->text : "") : 0; - if (v) fprintf(e->out,"%u",v->tag); - else fputs("0",e->out); -} - -static void m7_emit_raw_expr(FeEmitter *e, FeNode *n); - -static void m7_emit_contextual(FeEmitter *e, FeNode *n) -{ - FeType *ctx; - FeType *actual; - FeType *error_type; - ctx=n ? n->sem_context : 0; - actual=n ? n->sem_type : 0; - if (!ctx) { m7_emit_raw_expr(e,n); return; } - if (ctx->kind==FE_TYPE_OPTIONAL) { - if (fe_m7_is_null(n)) { - if (fe_m7_optional_uses_niche(ctx->elem)) fputs("0",e->out); - else { fputs(ctx->none_cname,e->out); fputs("()",e->out); } - return; - } - if (fe_m7_optional_uses_niche(ctx->elem)) { - m7_emit_raw_expr(e,n); - } else { - fputs(ctx->maker,e->out); fputc('(',e->out); - m7_emit_raw_expr(e,n); fputc(')',e->out); - } - return; - } - if (ctx->kind==FE_TYPE_ERROR_UNION) { - error_type=ctx->elem; - if (!error_type) error_type=fe_type_intern(&e->check->types,"core.Error"); - if (actual && fe_type_equal(actual,ctx->error_value)) { - if (ctx->error_value->kind==FE_TYPE_VOID) fputs("0",e->out); - else { - fputs(ctx->maker,e->out); fputs("(0, ",e->out); - m7_emit_raw_expr(e,n); fputc(')',e->out); - } - return; - } - if (actual && fe_type_equal(actual,error_type)) { - if (ctx->error_value->kind==FE_TYPE_VOID) - m7_emit_raw_expr(e,n); - else { - fputs(ctx->none_cname,e->out); fputc('(',e->out); - m7_emit_raw_expr(e,n); fputc(')',e->out); - } - return; - } - } - m7_emit_raw_expr(e,n); -} - -static void emit_expr(FeEmitter *e, FeNode *n) -{ - if (!n) { fputs("0",e->out); return; } - if (n->sem_context) m7_emit_contextual(e,n); - else m7_emit_raw_expr(e,n); -} - -static void emit_lvalue(FeEmitter *e, FeNode *n) -{ - FeType *bt; - if (!n) { fputs("fe_bad_lvalue",e->out); return; } - if (n->kind==FE_N_IDENT) { - fputs(cname(n,"fe_local"),e->out); - return; - } - /* A declaration names its own storage. The initializer for `let`/`var` is - emitted as a separate assignment statement, so the declaration node is - handed here as the target; without this it falls through to the raw - expression path, which emits a declaration as "0" and produces `0 = ...`. */ - if (n->kind==FE_N_LET || n->kind==FE_N_VAR || n->kind==FE_N_CONST) { - fputs(cname(n,"fe_local"),e->out); - return; - } - if (n->kind==FE_N_MEMBER) { - bt=n->a ? n->a->sem_type : 0; - if (n->text && strcmp(n->text,".?")==0) { - emit_expr(e,n); - return; - } - if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); - } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { - emit_expr(e,n->a); fputs("->",e->out); - fputs(n->b && n->b->text ? n->b->text : "member",e->out); - } else { - emit_lvalue(e,n->a); fputc('.',e->out); - fputs(n->b && n->b->text ? n->b->text : "member",e->out); - } - return; - } - if (n->kind==FE_N_INDEX) { - bt=n->a ? n->a->sem_type : 0; - emit_lvalue(e,n->a); - fputs(bt && bt->kind==FE_TYPE_ARRAY ? ".a[" : ".p[",e->out); - emit_expr(e,n->b); fputc(']',e->out); - return; - } - m7_emit_raw_expr(e,n); -} - -static void m7_emit_call(FeEmitter *e, FeNode *n) -{ - FeNode *x; - FeNode *call_param; - FeVariantType *v; - int special; - call_param=0; - special=0; - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"destroy")==0 && n->children) { - FeNode *arg=n->children; - fputs("(free(",e->out); emit_expr(e,arg); - if (arg->sem_type && arg->sem_type->kind==FE_TYPE_OWNED && - arg->sem_type->elem && arg->sem_type->elem->kind==FE_TYPE_SLICE) - fputs(".p",e->out); - fputc(')',e->out); - if (arg->kind==FE_N_IDENT) { - fputs(", ",e->out); emit_lvalue(e,arg); - if (arg->sem_type && arg->sem_type->elem && - arg->sem_type->elem->kind==FE_TYPE_SLICE) fputs(".p=0",e->out); - else fputs("=0",e->out); - fputs(", fe_live_",e->out); fputs(cname(arg,"owned"),e->out); - fputs("=0",e->out); - } - fputs(", 0)",e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"replace")==0 && n->children && - n->children->next && n->sem_type) { - fputs(n->sem_type->replace_cname ? n->sem_type->replace_cname : - "fe_bad_replace",e->out); - fputc('(',e->out); emit_expr(e,n->children); fputs(", ",e->out); - emit_expr(e,n->children->next); fputc(')',e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"create")==0 && n->children) { - FeType *created=n->children->sem_type; - FeType *owned=fe_type_owned(&e->check->types,created); - FeType *result=fe_type_error_union(&e->check->types,owned); - fputs(result->alloc_cname ? result->alloc_cname : "fe_bad_alloc",e->out); - fputc('(',e->out); emit_expr(e,n->children); fputc(')',e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"alloc_slice")==0 && n->children && - n->children->next) { - FeType *result=n->sem_type; - fputs(result && result->alloc_cname ? result->alloc_cname : - "fe_bad_slice_alloc",e->out); - fputc('(',e->out); emit_expr(e,n->children->next); fputc(')',e->out); - return; - } - if (n->text && (strcmp(n->text,"@print")==0 || - strcmp(n->text,"@fprint")==0 || strcmp(n->text,"@sprint")==0)) { - emit_m4_builtin(e,n); - return; - } - /* Every other builtin -- @size_of, @align_of and friends -- is lowered by - the core emitter. Without this the generic path below emits the call - verbatim, which is not C. */ - if (!n->a && n->text && n->text[0]=='@') { - emit_expr_core(e,n); - return; - } - /* Same for the built-in alias methods on str: the core emitter knows how to - lower `line.trim()`, the generic member path would emit `.trim()`. */ - if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"trim")==0 && !n->children) { - emit_expr_core(e,n); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"null_writer")==0) { - fputs("fe_m4_null_writer()",e->out); - return; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->sem_type && n->a->a->sem_type->kind==FE_TYPE_ENUM && - !n->a->a->sem_type->is_error) { - v=fe_type_variant(n->a->a->sem_type,n->a->b ? n->a->b->text : ""); - fputs(v ? v->maker : "fe_bad_variant",e->out); - } else if (n->a && n->a->kind==FE_N_MEMBER && n->sem_decl && - n->sem_decl->kind==FE_N_FN) { - FeNode *mp=n->sem_decl->a ? n->sem_decl->a->children : 0; - FeNode *ma; - fputs(cname(n->sem_decl,"fe_method"),e->out); fputc('(',e->out); - if (mp && mp->sem_type && mp->sem_type->kind==FE_TYPE_REF) { - fputc('&',e->out); emit_lvalue(e,n->a->a); - } else emit_expr(e,n->a->a); - for (ma=n->children;ma;ma=ma->next) { - fputs(", ",e->out); emit_expr(e,ma); - } - fputc(')',e->out); - return; - } else if (n->a) emit_expr(e,n->a); - else fputs(n->text ? n->text : "fe_builtin",e->out); - if (!special) { - if (n->sem_decl && n->sem_decl->kind==FE_N_FN && n->sem_decl->a) - call_param=n->sem_decl->a->children; - fputc('(',e->out); - for (x=n->children;x;x=x->next) { - FeType *want=call_param && call_param->a ? - fe_type_from_ast(&e->check->types,call_param->a) : 0; - if (x!=n->children) fputs(", ",e->out); - if (want && want->kind==FE_TYPE_SLICE && !want->ref_mut && - x->sem_type && x->sem_type->kind==FE_TYPE_SLICE && - x->sem_type->ref_mut) { - fputs(want->maker,e->out); fputc('(',e->out); - emit_expr(e,x); fputs(".p, ",e->out); - emit_expr(e,x); fputs(".n)",e->out); - } else emit_expr(e,x); - if (call_param) call_param=call_param->next; - } - fputc(')',e->out); - } -} - -/* `dst = ;` as a statement, avoiding a comma expression on the right. - - A consumed identifier lowers to `(fe_live_x=0, x)`. When dst is a struct, - Watcom crashes on a struct assignment whose right side is a comma expression - -- hard enough to take DOSBox-X down with it -- so clear the move flag as its - own statement and assign the plain name. */ -static void m7_emit_assign_stmt(FeEmitter *e, const char *dst, FeNode *src) -{ - if (src && src->kind==FE_N_IDENT && (src->flags & FE_OWN_NODE_CONSUMED) && - src->sem_type && type_needs_drop(src->sem_type)) { - pad(e); fputs("fe_live_",e->out); fputs(cname(src,"owned"),e->out); - fputs("=0;\n",e->out); - pad(e); fputs(dst,e->out); fputs(" = ",e->out); - fputs(cname(src,"fe_missing"),e->out); fputs(";\n",e->out); - return; - } - pad(e); fputs(dst,e->out); fputs(" = ",e->out); - emit_expr(e,src); fputs(";\n",e->out); -} - -static void m7_emit_raw_expr(FeEmitter *e, FeNode *n) -{ - FeType *bt; - FeVariantType *v; - const char *op; - FeM7LazyKind lazy; - if (!n) { fputs("0",e->out); return; } - /* No feature scan: the switch below handles the node kinds this emitter - changes and its default hands everything else to emit_expr_core, so the - same path serves a unit whether or not it mentions optionals. */ - switch (n->kind) { - case FE_N_IDENT: - if ((n->flags & FE_OWN_NODE_CONSUMED) && n->sem_type && - type_needs_drop(n->sem_type)) { - fputs("(fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs("=0, ",e->out); fputs(cname(n,"fe_missing"),e->out); - fputc(')',e->out); - } else fputs(cname(n,"fe_missing"),e->out); - break; - case FE_N_LITERAL: - if (fe_m7_is_null(n)) fputs("0",e->out); - else emit_expr_core(e,n); - break; - case FE_N_UNARY: - op=n->text ? n->text : ""; - if (strcmp(op,"try")==0) { - if (n->a && n->a->sem_type && - n->a->sem_type->kind==FE_TYPE_ERROR_UNION && - n->a->sem_type->error_value && - n->a->sem_type->error_value->kind!=FE_TYPE_VOID) { - fputs("(",e->out); fputs(n->aux_cname,e->out); - fputs(" = ",e->out); emit_expr(e,n->a); fputs(", ",e->out); - fputs(n->aux_cname,e->out); fputs(".v)",e->out); - } else emit_expr(e,n->a); - } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { - fputs("(&",e->out); emit_lvalue(e,n->a); fputc(')',e->out); - } else if (strcmp(op,"not")==0) { - fputs("(!",e->out); emit_expr(e,n->a); fputc(')',e->out); - } else { - fputc('(',e->out); fputs(op,e->out); emit_expr(e,n->a); - fputc(')',e->out); - } - break; - case FE_N_BINARY: - lazy=fe_m7_lazy_kind(n); - if (lazy==FE_M7_LAZY_ORELSE) { - FeType *opt=n->a ? n->a->sem_type : 0; - fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs("), ",e->out); - m7_emit_present(e,opt,n->aux_cname); fputs(" ? ",e->out); - if (fe_m7_optional_uses_niche(opt->elem)) fputs(n->aux_cname,e->out); - else { fputs(n->aux_cname,e->out); fputs(".v",e->out); } - fputs(" : ",e->out); emit_expr(e,n->b); fputc(')',e->out); - } else if (lazy==FE_M7_LAZY_CATCH && !n->c) { - FeType *res=n->a ? n->a->sem_type : 0; - fputs("((",e->out); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs("), ",e->out); - if (res && res->error_value && res->error_value->kind==FE_TYPE_VOID) { - fputs(n->aux_cname,e->out); fputs(" ? ",e->out); - emit_expr(e,n->b); fputs(" : 0)",e->out); - } else { - fputs(n->aux_cname,e->out); fputs(".e ? ",e->out); - emit_expr(e,n->b); fputs(" : ",e->out); - fputs(n->aux_cname,e->out); fputs(".v)",e->out); - } - } else if (lazy==FE_M7_LAZY_CATCH && n->c) { - fputs("0",e->out); - } else if ((n->text && (strcmp(n->text,"==")==0 || - strcmp(n->text,"!=")==0)) && - (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { - FeNode *value=fe_m7_is_null(n->a) ? n->b : n->a; - FeType *opt=value ? value->sem_type : 0; - if (opt && opt->kind==FE_TYPE_OPTIONAL && - !fe_m7_optional_uses_niche(opt->elem)) { - fputs("(!",e->out); emit_expr(e,value); fputs(".has)",e->out); - if (strcmp(n->text,"!=")==0) { - fputs(" == 0",e->out); - } - } else { - fputc('(',e->out); emit_expr(e,value); - fputs(strcmp(n->text,"==")==0 ? " == 0)" : " != 0)",e->out); - } - } else { - op=n->text ? n->text : "+"; - fputc('(',e->out); emit_expr(e,n->a); - if (strcmp(op,"and")==0) fputs(" && ",e->out); - else if (strcmp(op,"or")==0) fputs(" || ",e->out); - else fputs(op,e->out); - emit_expr(e,n->b); fputc(')',e->out); - } - break; - case FE_N_MEMBER: - bt=n->a ? n->a->sem_type : 0; - if (n->text && strcmp(n->text,".?")==0 && bt && - bt->kind==FE_TYPE_OPTIONAL) { - fputs(bt->unwrap_cname,e->out); fputc('(',e->out); - emit_expr(e,n->a); fputc(')',e->out); - } else if (bt && bt->kind==FE_TYPE_ENUM && bt->is_error) { - m7_emit_error_member(e,n); - } else if ((bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) && - n->b && n->b->text && strcmp(n->b->text,"^")==0) { - fputs("(*",e->out); emit_expr(e,n->a); fputc(')',e->out); - } else if (bt && (bt->kind==FE_TYPE_REF || bt->kind==FE_TYPE_OWNED)) { - emit_expr(e,n->a); fputs("->",e->out); - fputs(n->b && n->b->text ? n->b->text : "member",e->out); - } else if (bt && bt->kind==FE_TYPE_ENUM && !bt->is_error) { - v=fe_type_variant(bt,n->b ? n->b->text : ""); - if (v) { fputs(v->maker,e->out); fputs("()",e->out); } - else fputs("0",e->out); - } else { - emit_expr(e,n->a); fputc('.',e->out); - if (n->b) fputs(n->b->text ? n->b->text : "member",e->out); - } - break; - case FE_N_CALL: - m7_emit_call(e,n); - break; - case FE_N_TYPE: - if (n->text && strcmp(n->text,"as")==0) { - fputs("((",e->out); fputs(m7_c_type(e,n->sem_type),e->out); - fputc(')',e->out); emit_expr(e,n->a); fputc(')',e->out); - } else emit_expr(e,n->a); - break; - case FE_N_INDEX: - bt=n->a ? n->a->sem_type : 0; - if (n->c || !n->b) { - emit_expr_core(e,n); - } else if (bt && bt->indexer) { - fputs(bt->indexer,e->out); fputc('(',e->out); - emit_expr(e,n->a); fputs(", ",e->out); emit_expr(e,n->b); - fputc(')',e->out); - } else fputs("0",e->out); - break; - case FE_N_STRUCT_INIT: - case FE_N_ARRAY_INIT: - emit_expr_core(e,n); - break; - default: - emit_expr_core(e,n); - break; - } -} - -/* Emit the initializer for a `const` declaration. - - A string literal normally lowers to a maker call, but C89 requires the - initializer of an aggregate -- at file scope and for automatics alike -- to be - a constant expression, and the build runs with -za. Emit the slice braced - instead. Returns non-zero when it handled the initializer. */ -static int m7_emit_const_init(FeEmitter *e, FeNode *n) -{ - if (n->kind!=FE_N_CONST || !n->b || n->b->kind!=FE_N_LITERAL || - !n->b->text || n->b->text[0]!='"') return 0; - fputs("{ (const unsigned char*)",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(", sizeof(",e->out); - emit_c_literal(e->out,n->b->text,1); - fputs(")-1 }",e->out); - return 1; -} - -static void emit_decl(FeEmitter *e, FeNode *n) -{ - pad(e); fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); - fputs(cname(n,"fe_local"),e->out); - if (n->kind==FE_N_CONST && n->b) { - fputs(" = ",e->out); - if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); - } - fputs(";\n",e->out); - if ((n->kind==FE_N_LET || n->kind==FE_N_VAR) && n->sem_type && - type_needs_drop(n->sem_type)) { - pad(e); fputs("unsigned char fe_live_",e->out); - fputs(cname(n,"owned"),e->out); fputs("=0;\n",e->out); - } -} - -static void emit_owned_live(FeEmitter *e, FeNode *n, int value) -{ - if (n && n->sem_type && type_needs_drop(n->sem_type)) { - pad(e); fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fprintf(e->out,"=%d;\n",value); - } -} - -static void emit_value_drop(FeEmitter *e, FeNode *n) -{ - FeType *t; - t=n ? n->sem_type : 0; - if (!n || !t || !type_needs_drop(t) || - (n->flags & FE_OWN_NODE_CONSUMED) || - (n->flags & FE_OWN_NODE_DEFER_CAPTURE)) return; - pad(e); fputs("if (fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs(") { ",e->out); - if (t->kind==FE_TYPE_OWNED) { - if (t->elem && t->elem->kind==FE_TYPE_SLICE) { - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); - fputs(".p); ",e->out); - } else { - if (t->elem && type_needs_drop(t->elem) && t->elem->drop_cname) - fprintf(e->out,"%s(%s); ",t->elem->drop_cname,cname(n,"owned")); - fputs("free(",e->out); fputs(cname(n,"owned"),e->out); - fputs("); ",e->out); - } - } else if (t->drop_cname) { - fprintf(e->out,"%s(&%s); ",t->drop_cname,cname(n,"local")); - } - fputs("fe_live_",e->out); fputs(cname(n,"owned"),e->out); - fputs("=0; }\n",e->out); -} - -static void emit_cleanup_block(FeEmitter *e, FeNode *n) -{ - FeNode *x; - unsigned count; - unsigned index; - unsigned seen; - unsigned depth; - count=0; - seen=0xffffffffU; - for (depth=0;depthblock_depth;++depth) - if (e->block_stack[depth]==n) { - seen=e->block_seen[depth]; - break; - } - for (x=n ? n->children : 0,index=0;x;x=x->next,++index) - if (indexkind==FE_N_DEFER || x->kind==FE_N_LET || - x->kind==FE_N_VAR)) ++count; - while (count) { - index=0; - for (x=n->children;x;x=x->next) - if ((x->kind==FE_N_DEFER || x->kind==FE_N_LET || - x->kind==FE_N_VAR) && index++==count-1U) { - if (x->kind==FE_N_DEFER) emit_stmt(e,x->a); - else emit_value_drop(e,x); - break; - } - --count; - } -} - -static void emit_cleanup_to(FeEmitter *e, unsigned floor) -{ - unsigned i; - for (i=e->block_depth;i>floor;--i) - emit_cleanup_block(e,e->block_stack[i-1U]); -} - -static void emit_param_cleanup(FeEmitter *e) -{ - FeNode *p; - if (!e->current_fn || !e->current_fn->a) return; - for (p=e->current_fn->a->children;p;p=p->next) emit_value_drop(e,p); -} - -static void emit_cleanup_all(FeEmitter *e) -{ - emit_cleanup_to(e,0); - emit_param_cleanup(e); -} - -static void emit_error_return(FeEmitter *e, const char *error_expr) -{ - FeType *ret=e->current_ret; - if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && - ret->error_value->kind!=FE_TYPE_VOID) { - fputs("return ",e->out); fputs(ret->none_cname,e->out); - fputc('(',e->out); fputs(error_expr,e->out); fputs(");\n",e->out); - } else { - fputs("return ",e->out); fputs(error_expr,e->out); fputs(";\n",e->out); - } -} - -static void m7_emit_try_error_check(FeEmitter *e, FeNode *n) -{ - FeType *result=n->a ? n->a->sem_type : 0; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - emit_cleanup_all(e); - pad(e); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) { - char error[192]; - sprintf(error,"%s.e",n->aux_cname); - emit_error_return(e,error); - } else emit_error_return(e,n->aux_cname); - --e->indent; pad(e); fputs("}\n",e->out); -} - -static void m7_emit_catch_block(FeEmitter *e, FeNode *n, - FeNode *target) -{ - FeType *result=n->a ? n->a->sem_type : 0; - FeNode *binding=n->b; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); fputs(n->aux_cname,e->out); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - if (binding && binding->cname) { - pad(e); fputs("unsigned short ",e->out); fputs(binding->cname,e->out); - fputs(" = ",e->out); fputs(n->aux_cname,e->out); - if (result && result->error_value && result->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(";\n",e->out); - } - emit_stmt(e,n->c); - --e->indent; pad(e); fputs("}",e->out); - if (target && result && result->error_value && - result->error_value->kind!=FE_TYPE_VOID) { - fputs(" else {\n",e->out); ++e->indent; - pad(e); emit_lvalue(e,target); fputs(" = ",e->out); - fputs(n->aux_cname,e->out); fputs(".v;\n",e->out); - emit_owned_live(e,target,1); - --e->indent; pad(e); fputs("}",e->out); - } - fputc('\n',e->out); -} - -static void m7_emit_optional_match(FeEmitter *e, FeNode *n) -{ - FeType *opt=n->a ? n->a->sem_type : 0; - FeNode *arm; - FeNode *binding; - int first; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - first=1; - for (arm=n->children;arm;arm=arm->next) { - if (arm->text && strcmp(arm->text,"Some")==0) { - pad(e); if (!first) fputs("else ",e->out); - fputs("if (",e->out); m7_emit_present(e,opt,n->aux_cname); - fputs(") {\n",e->out); ++e->indent; - binding=arm->children; - if (binding && binding->cname) { - pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); - fputc(' ',e->out); fputs(binding->cname,e->out); fputs(" = ",e->out); - m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); - } - if (arm->a) emit_stmt(e,arm->a); - --e->indent; pad(e); fputs("}\n",e->out); - first=0; - } else if (arm->text && strcmp(arm->text,"None")==0) { - pad(e); if (!first) fputs("else ",e->out); - fputs("if (!",e->out); m7_emit_present(e,opt,n->aux_cname); - fputs(") {\n",e->out); ++e->indent; - if (arm->a) emit_stmt(e,arm->a); - --e->indent; pad(e); fputs("}\n",e->out); - first=0; - } else if (arm->text && strcmp(arm->text,"_")==0) { - pad(e); if (!first) fputs("else ",e->out); - fputs("{\n",e->out); ++e->indent; - if (arm->a) emit_stmt(e,arm->a); - --e->indent; pad(e); fputs("}\n",e->out); - first=0; - } - } -} - -static void m7_emit_if_let(FeEmitter *e, FeNode *n) -{ - FeType *opt=n->a ? n->a->sem_type : 0; - FeNode *binding=n->children; - int some=n->aux_text && strcmp(n->aux_text,"Some")==0; - pad(e); fputs(n->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); - if (!some) fputc('!',e->out); - m7_emit_present(e,opt,n->aux_cname); fputs(") {\n",e->out); - ++e->indent; - if (some && binding && binding->cname) { - pad(e); fputs(m7_c_type(e,binding->sem_type),e->out); fputc(' ',e->out); - fputs(binding->cname,e->out); fputs(" = ",e->out); - m7_emit_payload_var(e,opt,n->aux_cname); fputs(";\n",e->out); - } - if (n->b) emit_stmt(e,n->b); - --e->indent; pad(e); fputs("}",e->out); - if (n->c) { - fputs(" else ",e->out); - emit_stmt(e,n->c); - } - fputc('\n',e->out); -} - -static void emit_block(FeEmitter *e, FeNode *n) -{ - FeNode *x; - unsigned seen; - if (!n) { - pad(e); fputs("{}",e->out); return; - } - pad(e); fputs("{\n",e->out); ++e->indent; - if (e->block_depth<32U) { - e->block_stack[e->block_depth]=n; - e->block_seen[e->block_depth]=0; - ++e->block_depth; - } - for (x=n->children;x;x=x->next) - if (x->kind==FE_N_LET || x->kind==FE_N_VAR || x->kind==FE_N_CONST) - emit_decl(e,x); - if (e->current_fn && e->current_fn->c==n) { - if (e->current_fn->a) { - FeNode *p; - for (p=e->current_fn->a->children;p;p=p->next) - if (p->sem_type && type_needs_drop(p->sem_type)) { - pad(e); fputs("unsigned char fe_live_",e->out); - fputs(cname(p,"owned"),e->out); fputs("=1;\n",e->out); - } - } - m7_emit_temp_decls(e,n); - } - if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs(m7_c_type(e,e->current_ret),e->out); - fputs(" fe_return_value;\n",e->out); - } - seen=0; - for (x=n->children;x;x=x->next) { - ++seen; - if (e->block_depth) e->block_seen[e->block_depth-1U]=seen; - emit_stmt(e,x); - } - --e->indent; - emit_cleanup_block(e,n); - if (e->current_fn && e->current_fn->c==n) emit_param_cleanup(e); - if (e->block_depth) --e->block_depth; - if (e->fallthrough_block==n) { - pad(e); fputs("return 0;\n",e->out); - e->fallthrough_block=0; - } - pad(e); fputc('}',e->out); -} - -static void emit_stmt(FeEmitter *e, FeNode *n) -{ - FeType *result; - if (!n) return; - switch (n->kind) { - case FE_N_BLOCK: - emit_block(e,n); fputc('\n',e->out); break; - case FE_N_LET: - case FE_N_VAR: - if (n->b) { - if (fe_m7_is_try(n->b)) { - m7_emit_try_error_check(e,n->b); - pad(e); emit_lvalue(e,n); fputs(" = ",e->out); - fputs(n->b->aux_cname,e->out); - result=n->b->a ? n->b->a->sem_type : 0; - if (result && result->error_value && - result->error_value->kind!=FE_TYPE_VOID) fputs(".v",e->out); - fputs(";\n",e->out); emit_owned_live(e,n,1); - } else if (n->b->kind==FE_N_BINARY && n->b->c && - fe_m7_lazy_kind(n->b)==FE_M7_LAZY_CATCH) { - m7_emit_catch_block(e,n->b,n); - } else { - pad(e); emit_lvalue(e,n); fputs(" = ",e->out); - emit_expr(e,n->b); fputs(";\n",e->out); - emit_owned_live(e,n,1); - } - } - break; - case FE_N_ASSIGN: - if (n->a && n->a->kind==FE_N_IDENT) emit_value_drop(e,n->a); - pad(e); emit_lvalue(e,n->a); fputc(' ',e->out); - fputs(n->text ? n->text : "=",e->out); fputc(' ',e->out); - emit_expr(e,n->b); fputs(";\n",e->out); - if (n->a && n->a->kind==FE_N_IDENT) emit_owned_live(e,n->a,1); - break; - case FE_N_EXPR_STMT: - if (fe_m7_is_try(n->a)) { - m7_emit_try_error_check(e,n->a); - } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && - fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { - m7_emit_catch_block(e,n->a,0); - } else { - pad(e); emit_expr(e,n->a); fputs(";\n",e->out); - } - break; - case FE_N_DEFER: - break; - case FE_N_RETURN: - if (n->a && fe_m7_is_try(n->a)) { - FeNode *tr=n->a; - FeType *res=tr->a ? tr->a->sem_type : 0; - m7_emit_try_error_check(e,tr); - if (e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs("fe_return_value = ",e->out); - if (e->current_ret->kind==FE_TYPE_ERROR_UNION && - e->current_ret->error_value && - e->current_ret->error_value->kind!=FE_TYPE_VOID) { - fputs(e->current_ret->maker,e->out); fputs("(0, ",e->out); - fputs(tr->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".v",e->out); - fputc(')',e->out); - } else { - fputs(tr->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".v",e->out); - } - fputs(";\n",e->out); - } - emit_cleanup_all(e); - pad(e); fputs("return fe_return_value;\n",e->out); - } else if (n->a && n->a->kind==FE_N_BINARY && n->a->c && - fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH) { - /* A value catch-block is lowered as a temporary local success - assignment; the handler is required by the checker to exit. */ - FeNode *cx=n->a; - FeType *res=cx->a ? cx->a->sem_type : 0; - pad(e); fputs(cx->aux_cname,e->out); fputs(" = ",e->out); - emit_expr(e,cx->a); fputs(";\n",e->out); - pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - if (cx->b && cx->b->cname) { - pad(e); fputs("unsigned short ",e->out); fputs(cx->b->cname,e->out); - fputs(" = ",e->out); fputs(cx->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".e",e->out); - fputs(";\n",e->out); - } - emit_stmt(e,cx->c); - --e->indent; pad(e); fputs("}\n",e->out); - pad(e); fputs("fe_return_value = ",e->out); - fputs(cx->aux_cname,e->out); - if (res && res->error_value && res->error_value->kind!=FE_TYPE_VOID) - fputs(".v",e->out); - fputs(";\n",e->out); - emit_cleanup_all(e); - pad(e); fputs("return fe_return_value;\n",e->out); - } else if (n->a && n->a->kind==FE_N_BINARY && !n->a->c && - fe_m7_lazy_kind(n->a)==FE_M7_LAZY_CATCH && - e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - /* Short catch in return position. As an expression this lowers to - `((tmp = X), tmp.e ? fallback : tmp.v)`, and when X carries a move - it becomes a struct assignment whose right side is itself a comma - expression -- which crashes wcc386 hard enough to take DOSBox-X - down with it. The same lowering as statements is also plainer. */ - FeNode *cx=n->a; - FeType *res=cx->a ? cx->a->sem_type : 0; - int has_value=res && res->error_value && - res->error_value->kind!=FE_TYPE_VOID; - m7_emit_assign_stmt(e,cx->aux_cname,cx->a); - pad(e); fputs("if (",e->out); fputs(cx->aux_cname,e->out); - if (has_value) fputs(".e",e->out); - fputs(") {\n",e->out); ++e->indent; - pad(e); fputs("fe_return_value = ",e->out); - emit_expr(e,cx->b); fputs(";\n",e->out); - --e->indent; pad(e); fputs("} else {\n",e->out); ++e->indent; - pad(e); fputs("fe_return_value = ",e->out); - fputs(cx->aux_cname,e->out); - if (has_value) fputs(".v",e->out); - fputs(";\n",e->out); - --e->indent; pad(e); fputs("}\n",e->out); - emit_cleanup_all(e); - pad(e); fputs("return fe_return_value;\n",e->out); - } else { - if (n->a && e->current_ret && e->current_ret->kind!=FE_TYPE_VOID) { - pad(e); fputs("fe_return_value = ",e->out); - emit_expr(e,n->a); fputs(";\n",e->out); - } - emit_cleanup_all(e); - pad(e); fputs("return",e->out); - if (n->a) fputs(" fe_return_value",e->out); - fputs(";\n",e->out); - } - break; - case FE_N_IF: - if (n->text && strcmp(n->text,"if let")==0) { - m7_emit_if_let(e,n); - } else { - pad(e); fputs("if (",e->out); emit_expr(e,n->a); fputs(") ",e->out); - emit_block(e,n->b); - if (n->c) { - fputs(" else ",e->out); - if (n->c->kind==FE_N_IF) emit_stmt(e,n->c); - else emit_block(e,n->c); - } - fputc('\n',e->out); - } - break; - case FE_N_MATCH: - if (n->a && n->a->sem_type && n->a->sem_type->kind==FE_TYPE_OPTIONAL) - m7_emit_optional_match(e,n); - else emit_match(e,n,0); - break; - case FE_N_BREAK: - case FE_N_CONTINUE: - if (e->loop_depth) { - emit_cleanup_to(e,e->loop_floor[e->loop_depth-1U]); - pad(e); fputs(n->kind==FE_N_BREAK ? "break;\n" : "continue;\n",e->out); - } - break; - case FE_N_WHILE: - pad(e); fputs("while (",e->out); emit_expr(e,n->a); fputs(") ",e->out); - if (e->loop_depth<16U) e->loop_floor[e->loop_depth++]=e->block_depth; - emit_block(e,n->b); - if (e->loop_depth) --e->loop_depth; - fputc('\n',e->out); - break; - case FE_N_FOR: - emit_stmt_core(e,n); - break; - default: - emit_stmt_core(e,n); - break; - } -} - -static void emit_fn(FeEmitter *e, FeNode *fn, int prototype) -{ - FeNode *p; - FeType *old_ret; - FeNode *old_fn; - fputs(m7_c_type(e,fn->sem_type ? fn->sem_type : - (fn->b ? fe_type_from_ast(&e->check->types,fn->b) : - fe_type_intern(&e->check->types,"void"))),e->out); - fputc(' ',e->out); fputs(cname(fn,"fe_fn"),e->out); fputc('(',e->out); - p=fn->a ? fn->a->children : 0; - if (!p) fputs("void",e->out); - while (p) { - if (p!=fn->a->children) fputs(", ",e->out); - fputs(m7_c_type(e,p->sem_type ? p->sem_type : - fe_type_from_ast(&e->check->types,p->a)),e->out); - fputc(' ',e->out); fputs(cname(p,"fe_arg"),e->out); - p=p->next; - } - fputc(')',e->out); - if (prototype) { fputs(";\n",e->out); return; } - old_ret=e->current_ret; - old_fn=e->current_fn; - e->current_ret=fn->sem_type; - e->current_fn=fn; - m7_prepare_temps(e,fn->c); - fputc(' ',e->out); - if (fn->sem_type && fn->sem_type->kind==FE_TYPE_ERROR_UNION && - fn->sem_type->error_value && fn->sem_type->error_value->kind==FE_TYPE_VOID) - e->fallthrough_block=fn->c; - emit_block(e,fn->c); - e->current_ret=old_ret; - e->current_fn=old_fn; - fputc('\n',e->out); -} - -static void emit_main_wrapper(FeEmitter *e, FeNode *fn) -{ - FeType *ret=fn->sem_type; - if (ret && ret->kind==FE_TYPE_ERROR_UNION && ret->error_value && - ret->error_value->kind!=FE_TYPE_VOID) { - fputs("int main(void) { ",e->out); fputs(m7_c_type(e,ret),e->out); - fputs(" r = ",e->out); fputs(cname(fn,"fe_main"),e->out); - fputs("(); return r.e ? 1 : 0; }\n",e->out); - } else emit_main_wrapper_core(e,fn); -} - -void fe_emit_c_program(FeEmitter *e) -{ - FeNode *n; - FeNode *main_fn; - FeType *type; - int need_m4; - main_fn=0; - need_m4=node_uses_m4(e->check->ast->root); - for (type=e->check->types.types;type;type=type->next) - if (strcmp(type->name,"io.Writer")==0) need_m4=1; - /* See emit_c.c: stdio only comes in with the M4 writer runtime. */ - fputs("/* generated by fec M7 */\n#include \n#include \n#include \n",e->out); - if (need_m4) fputs("#include \n",e->out); - fputs("typedef char fe_assert_u8[(sizeof(unsigned char)==1) ? 1 : -1];\ntypedef char fe_assert_u16[(sizeof(unsigned short)==2) ? 1 : -1];\ntypedef char fe_assert_u32[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); - if (e->pointer_bits==16) - fputs("typedef char fe_assert_usize[(sizeof(unsigned short)==2) ? 1 : -1];\n",e->out); - else - fputs("typedef char fe_assert_usize[(sizeof(unsigned long)==4) ? 1 : -1];\n",e->out); - fputs("static void fe_trap_bounds(void) { abort(); }\nstatic unsigned short fe_error_temp;\n\n",e->out); - emit_type_defs(e); - if (need_m4) emit_m4_runtime(e); - m7_emit_type_helpers(e); - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) { - if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { - fputs(m7_c_type(e,n->sem_type),e->out); fputc(' ',e->out); - fputs(cname(n,"fe_global"),e->out); - if (n->b) { - fputs(" = ",e->out); - if (!m7_emit_const_init(e,n)) emit_expr(e,n->b); - } - fputs(";\n",e->out); - } - } - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) { - emit_fn(e,n,1); - if (n->text && strcmp(n->text,"main")==0) main_fn=n; - } - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) { - FeNode *m; - for (m=n->children;m;m=m->next) - if (m->kind==FE_N_FN) emit_fn(e,m,1); - } - fputc('\n',e->out); - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) emit_fn(e,n,0); - for (n=e->check->ast->root ? e->check->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) { - FeNode *m; - for (m=n->children;m;m=m->next) - if (m->kind==FE_N_FN) emit_fn(e,m,0); - } - if (main_fn) { fputc('\n',e->out); emit_main_wrapper(e,main_fn); } -} diff --git a/fec/src/emit_c.h b/fec/src/emit_c.h deleted file mode 100644 index 8be10e6..0000000 --- a/fec/src/emit_c.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef FE_EMIT_C_H -#define FE_EMIT_C_H - -#include "check.h" -#include "own.h" - -typedef struct FeEmitter { - FILE *out; - FeCheck *check; - unsigned pointer_bits; - int indent; - int no_checks; - unsigned temp_serial; - FeNode *fallthrough_block; - FeNode *block_stack[32]; - unsigned block_seen[32]; - unsigned block_depth; - unsigned loop_floor[16]; - unsigned loop_depth; - FeType *current_ret; - FeNode *current_fn; -} FeEmitter; - -void fe_emit_c_init(FeEmitter *e, FILE *out, FeCheck *check, - unsigned pointer_bits, int no_checks); -void fe_emit_c_program(FeEmitter *e); - -#endif diff --git a/fec/src/lower.c b/fec/src/lower.c deleted file mode 100644 index abd6942..0000000 --- a/fec/src/lower.c +++ /dev/null @@ -1,227 +0,0 @@ -#include "lower.h" -#include - -static int lower_grow_scopes(FeLowerPlan *plan) -{ - FeLowerScope *items; - unsigned capacity; - if (plan->scope_count < plan->scope_capacity) return 1; - capacity = plan->scope_capacity ? plan->scope_capacity * 2U : 8U; - items = (FeLowerScope *)fe_arena_alloc(plan->arena, - capacity * sizeof(FeLowerScope)); - if (!items) return 0; - if (plan->scopes) - memcpy(items, plan->scopes, - plan->scope_count * sizeof(FeLowerScope)); - plan->scopes = items; - plan->scope_capacity = capacity; - return 1; -} - -static int lower_grow_cleanups(FeLowerPlan *plan) -{ - FeLowerCleanup *items; - unsigned capacity; - if (plan->cleanup_count < plan->cleanup_capacity) return 1; - capacity = plan->cleanup_capacity ? plan->cleanup_capacity * 2U : 16U; - items = (FeLowerCleanup *)fe_arena_alloc(plan->arena, - capacity * sizeof(FeLowerCleanup)); - if (!items) return 0; - if (plan->cleanups) - memcpy(items, plan->cleanups, - plan->cleanup_count * sizeof(FeLowerCleanup)); - plan->cleanups = items; - plan->cleanup_capacity = capacity; - return 1; -} - -static unsigned lower_add_scope(FeLowerPlan *plan, FeNode *block, - unsigned parent) -{ - FeLowerScope *scope; - unsigned index; - if (!lower_grow_scopes(plan)) return FE_LOWER_NO_SCOPE; - index = plan->scope_count++; - scope = &plan->scopes[index]; - scope->block = block; - scope->parent = parent; - scope->ordinal = plan->next_ordinal++; - return index; -} - -static int lower_add_cleanup(FeLowerPlan *plan, unsigned scope, - FeLowerCleanupKind kind, FeNode *node, - FeNode *decl, FeType *type) -{ - FeLowerCleanup *cleanup; - if (!lower_grow_cleanups(plan)) return 0; - cleanup = &plan->cleanups[plan->cleanup_count++]; - cleanup->kind = kind; - cleanup->scope = scope; - cleanup->ordinal = plan->next_ordinal++; - cleanup->node = node; - cleanup->decl = decl; - cleanup->type = type; - return 1; -} - -int fe_lower_type_needs_drop(const FeType *type) -{ - unsigned i; - unsigned j; - if (!type) return 0; - if (type->kind == FE_TYPE_OWNED) return 1; - if (type->kind == FE_TYPE_OPTIONAL) - return fe_lower_type_needs_drop(type->elem); - if (type->kind == FE_TYPE_ERROR_UNION) - return fe_lower_type_needs_drop(type->error_value); - if (type->kind == FE_TYPE_ARRAY) - return fe_lower_type_needs_drop(type->elem); - if (type->kind == FE_TYPE_STRUCT) { - if (type->has_drop) return 1; - for (i = 0; i < type->field_count; ++i) - if (fe_lower_type_needs_drop(type->fields[i].type)) return 1; - return 0; - } - if (type->kind == FE_TYPE_ENUM) { - for (i = 0; i < type->variant_count; ++i) - for (j = 0; j < type->variants[i].field_count; ++j) - if (fe_lower_type_needs_drop(type->variants[i].fields[j].type)) - return 1; - } - return 0; -} - -static int lower_build_node(FeLowerPlan *plan, FeNode *node, - unsigned scope); - -static int lower_build_list(FeLowerPlan *plan, FeNode *node, - unsigned scope) -{ - while (node) { - if (!lower_build_node(plan, node, scope)) return 0; - node = node->next; - } - return 1; -} - -static int lower_build_block(FeLowerPlan *plan, FeNode *block, - unsigned parent) -{ - unsigned scope; - if (!block || block->kind != FE_N_BLOCK) return 1; - scope = lower_add_scope(plan, block, parent); - if (scope == FE_LOWER_NO_SCOPE) return 0; - return lower_build_list(plan, block->children, scope); -} - -static int lower_build_node(FeLowerPlan *plan, FeNode *node, - unsigned scope) -{ - FeNode *child; - FeType *type; - if (!node) return 1; - if (node->kind == FE_N_BLOCK) - return lower_build_block(plan, node, scope); - if (node->kind == FE_N_DEFER) { - if (!lower_add_cleanup(plan, scope, FE_LOWER_CLEANUP_DEFER, - node->a, node, 0)) - return 0; - return lower_build_node(plan, node->a, scope); - } - if (node->kind == FE_N_LET || node->kind == FE_N_VAR || - node->kind == FE_N_CONST) { - type = node->sem_type; - if (fe_lower_type_needs_drop(type)) - if (!lower_add_cleanup(plan, scope, FE_LOWER_CLEANUP_DROP, - node, node, type)) - return 0; - } - if (!lower_build_node(plan, node->a, scope)) return 0; - if (!lower_build_node(plan, node->b, scope)) return 0; - if (!lower_build_node(plan, node->c, scope)) return 0; - for (child = node->children; child; child = child->next) - if (!lower_build_node(plan, child, scope)) return 0; - return 1; -} - -void fe_lower_plan_init(FeLowerPlan *plan, FeArena *arena) -{ - if (!plan) return; - plan->arena = arena; - plan->fn = 0; - plan->scopes = 0; - plan->scope_count = 0; - plan->scope_capacity = 0; - plan->cleanups = 0; - plan->cleanup_count = 0; - plan->cleanup_capacity = 0; - plan->next_ordinal = 0; -} - -int fe_lower_plan_build(FeLowerPlan *plan, FeNode *fn) -{ - if (!plan || !plan->arena || !fn || fn->kind != FE_N_FN) return 0; - plan->fn = fn; - plan->scopes = 0; - plan->scope_count = 0; - plan->scope_capacity = 0; - plan->cleanups = 0; - plan->cleanup_count = 0; - plan->cleanup_capacity = 0; - plan->next_ordinal = 0; - if (!fn->c) return 1; - return lower_build_block(plan, fn->c, FE_LOWER_NO_SCOPE); -} - -unsigned fe_lower_scope_for_block(const FeLowerPlan *plan, - const FeNode *block) -{ - unsigned i; - if (!plan || !block) return FE_LOWER_NO_SCOPE; - for (i = 0; i < plan->scope_count; ++i) - if (plan->scopes[i].block == block) return i; - return FE_LOWER_NO_SCOPE; -} - -unsigned fe_lower_collect_cleanups(const FeLowerPlan *plan, - const FeNode *from_block, - const FeNode *stop_block, - const FeLowerCleanup **out, - unsigned out_capacity) -{ - unsigned scope; - unsigned stop; - unsigned i; - unsigned count; - if (!plan || !from_block) return 0; - scope = fe_lower_scope_for_block(plan, from_block); - stop = stop_block ? fe_lower_scope_for_block(plan, stop_block) : - FE_LOWER_NO_SCOPE; - count = 0; - while (scope != FE_LOWER_NO_SCOPE && scope != stop) { - for (i = plan->cleanup_count; i > 0; --i) { - if (plan->cleanups[i - 1U].scope != scope) continue; - if (out && count < out_capacity) - out[count] = &plan->cleanups[i - 1U]; - ++count; - } - scope = plan->scopes[scope].parent; - } - return count; -} - -int fe_lower_exit_runs_cleanup(FeLowerExitKind kind) -{ - return kind == FE_LOWER_EXIT_FALLTHROUGH || - kind == FE_LOWER_EXIT_RETURN || - kind == FE_LOWER_EXIT_ERROR_RETURN || - kind == FE_LOWER_EXIT_BREAK || - kind == FE_LOWER_EXIT_CONTINUE; -} - -int fe_lower_exit_leaves_function(FeLowerExitKind kind) -{ - return kind == FE_LOWER_EXIT_RETURN || - kind == FE_LOWER_EXIT_ERROR_RETURN; -} diff --git a/fec/src/lower.h b/fec/src/lower.h deleted file mode 100644 index bb2e544..0000000 --- a/fec/src/lower.h +++ /dev/null @@ -1,61 +0,0 @@ -#ifndef FE_LOWER_H -#define FE_LOWER_H - -#include "types.h" - -#define FE_LOWER_NO_SCOPE ((unsigned)~0U) - -typedef enum FeLowerExitKind { - FE_LOWER_EXIT_FALLTHROUGH = 0, - FE_LOWER_EXIT_RETURN, - FE_LOWER_EXIT_ERROR_RETURN, - FE_LOWER_EXIT_BREAK, - FE_LOWER_EXIT_CONTINUE -} FeLowerExitKind; - -typedef enum FeLowerCleanupKind { - FE_LOWER_CLEANUP_DROP = 0, - FE_LOWER_CLEANUP_DEFER -} FeLowerCleanupKind; - -typedef struct FeLowerScope { - FeNode *block; - unsigned parent; - unsigned ordinal; -} FeLowerScope; - -typedef struct FeLowerCleanup { - FeLowerCleanupKind kind; - unsigned scope; - unsigned ordinal; - FeNode *node; - FeNode *decl; - FeType *type; -} FeLowerCleanup; - -typedef struct FeLowerPlan { - FeArena *arena; - FeNode *fn; - FeLowerScope *scopes; - unsigned scope_count; - unsigned scope_capacity; - FeLowerCleanup *cleanups; - unsigned cleanup_count; - unsigned cleanup_capacity; - unsigned next_ordinal; -} FeLowerPlan; - -void fe_lower_plan_init(FeLowerPlan *plan, FeArena *arena); -int fe_lower_plan_build(FeLowerPlan *plan, FeNode *fn); -unsigned fe_lower_scope_for_block(const FeLowerPlan *plan, - const FeNode *block); -unsigned fe_lower_collect_cleanups(const FeLowerPlan *plan, - const FeNode *from_block, - const FeNode *stop_block, - const FeLowerCleanup **out, - unsigned out_capacity); -int fe_lower_type_needs_drop(const FeType *type); -int fe_lower_exit_runs_cleanup(FeLowerExitKind kind); -int fe_lower_exit_leaves_function(FeLowerExitKind kind); - -#endif diff --git a/fec/tests/m4/bad-ari.fe b/fec/tests/format/bad-ari.fe similarity index 100% rename from fec/tests/m4/bad-ari.fe rename to fec/tests/format/bad-ari.fe diff --git a/fec/tests/m4/bad-bufw.fe b/fec/tests/format/bad-bufw.fe similarity index 100% rename from fec/tests/m4/bad-bufw.fe rename to fec/tests/format/bad-bufw.fe diff --git a/fec/tests/m4/bad-cls.fe b/fec/tests/format/bad-cls.fe similarity index 100% rename from fec/tests/m4/bad-cls.fe rename to fec/tests/format/bad-cls.fe diff --git a/fec/tests/m4/bad-many.fe b/fec/tests/format/bad-many.fe similarity index 100% rename from fec/tests/m4/bad-many.fe rename to fec/tests/format/bad-many.fe diff --git a/fec/tests/m4/bad-open.fe b/fec/tests/format/bad-open.fe similarity index 100% rename from fec/tests/m4/bad-open.fe rename to fec/tests/format/bad-open.fe diff --git a/fec/tests/m4/bad-run.fe b/fec/tests/format/bad-run.fe similarity index 100% rename from fec/tests/m4/bad-run.fe rename to fec/tests/format/bad-run.fe diff --git a/fec/tests/m4/bad-try.fe b/fec/tests/format/bad-try.fe similarity index 100% rename from fec/tests/m4/bad-try.fe rename to fec/tests/format/bad-try.fe diff --git a/fec/tests/m4/bad-type.fe b/fec/tests/format/bad-type.fe similarity index 100% rename from fec/tests/m4/bad-type.fe rename to fec/tests/format/bad-type.fe diff --git a/fec/tests/m4/bad-verb.fe b/fec/tests/format/bad-verb.fe similarity index 100% rename from fec/tests/m4/bad-verb.fe rename to fec/tests/format/bad-verb.fe diff --git a/fec/tests/m4/bad-writ.fe b/fec/tests/format/bad-writ.fe similarity index 100% rename from fec/tests/m4/bad-writ.fe rename to fec/tests/format/bad-writ.fe diff --git a/fec/tests/m4/format.fe b/fec/tests/format/ok-format.fe similarity index 100% rename from fec/tests/m4/format.fe rename to fec/tests/format/ok-format.fe diff --git a/fec/tests/m4/prop.fe b/fec/tests/format/ok-prop.fe similarity index 100% rename from fec/tests/m4/prop.fe rename to fec/tests/format/ok-prop.fe diff --git a/fec/tests/m4/try-fpr.fe b/fec/tests/format/ok-try-fpr.fe similarity index 100% rename from fec/tests/m4/try-fpr.fe rename to fec/tests/format/ok-try-fpr.fe diff --git a/fec/tests/m9/README.md b/fec/tests/generic/README.md similarity index 100% rename from fec/tests/m9/README.md rename to fec/tests/generic/README.md diff --git a/fec/tests/m9/badarg.fe b/fec/tests/generic/badarg.fe similarity index 100% rename from fec/tests/m9/badarg.fe rename to fec/tests/generic/badarg.fe diff --git a/fec/tests/m9/badarity.fe b/fec/tests/generic/badarity.fe similarity index 100% rename from fec/tests/m9/badarity.fe rename to fec/tests/generic/badarity.fe diff --git a/fec/tests/m9/badbody.fe b/fec/tests/generic/badbody.fe similarity index 100% rename from fec/tests/m9/badbody.fe rename to fec/tests/generic/badbody.fe diff --git a/fec/tests/m9/baddepth.fe b/fec/tests/generic/baddepth.fe similarity index 100% rename from fec/tests/m9/baddepth.fe rename to fec/tests/generic/baddepth.fe diff --git a/fec/tests/m9/baddist.fe b/fec/tests/generic/baddist.fe similarity index 100% rename from fec/tests/m9/baddist.fe rename to fec/tests/generic/baddist.fe diff --git a/fec/tests/m9/badfew.fe b/fec/tests/generic/badfew.fe similarity index 100% rename from fec/tests/m9/badfew.fe rename to fec/tests/generic/badfew.fe diff --git a/fec/tests/m9/badinfer.fe b/fec/tests/generic/badinfer.fe similarity index 100% rename from fec/tests/m9/badinfer.fe rename to fec/tests/generic/badinfer.fe diff --git a/fec/tests/m9/badop.fe b/fec/tests/generic/badop.fe similarity index 100% rename from fec/tests/m9/badop.fe rename to fec/tests/generic/badop.fe diff --git a/fec/tests/m9/badtype.fe b/fec/tests/generic/badtype.fe similarity index 100% rename from fec/tests/m9/badtype.fe rename to fec/tests/generic/badtype.fe diff --git a/fec/tests/m9/badvalue.fe b/fec/tests/generic/badvalue.fe similarity index 100% rename from fec/tests/m9/badvalue.fe rename to fec/tests/generic/badvalue.fe diff --git a/fec/tests/m9/defscope/lib.fe b/fec/tests/generic/defscope/lib.fe similarity index 100% rename from fec/tests/m9/defscope/lib.fe rename to fec/tests/generic/defscope/lib.fe diff --git a/fec/tests/m9/defscope/main.fe b/fec/tests/generic/defscope/main.fe similarity index 100% rename from fec/tests/m9/defscope/main.fe rename to fec/tests/generic/defscope/main.fe diff --git a/fec/tests/m9/okalias.fe b/fec/tests/generic/okalias.fe similarity index 100% rename from fec/tests/m9/okalias.fe rename to fec/tests/generic/okalias.fe diff --git a/fec/tests/m9/okbox.fe b/fec/tests/generic/okbox.fe similarity index 100% rename from fec/tests/m9/okbox.fe rename to fec/tests/generic/okbox.fe diff --git a/fec/tests/m9/okdedup.fe b/fec/tests/generic/okdedup.fe similarity index 100% rename from fec/tests/m9/okdedup.fe rename to fec/tests/generic/okdedup.fe diff --git a/fec/tests/m9/okid.fe b/fec/tests/generic/okid.fe similarity index 100% rename from fec/tests/m9/okid.fe rename to fec/tests/generic/okid.fe diff --git a/fec/tests/m9/okisint.fe b/fec/tests/generic/okisint.fe similarity index 100% rename from fec/tests/m9/okisint.fe rename to fec/tests/generic/okisint.fe diff --git a/fec/tests/m9/okmulti.fe b/fec/tests/generic/okmulti.fe similarity index 100% rename from fec/tests/m9/okmulti.fe rename to fec/tests/generic/okmulti.fe diff --git a/fec/tests/m9/oknested.fe b/fec/tests/generic/oknested.fe similarity index 100% rename from fec/tests/m9/oknested.fe rename to fec/tests/generic/oknested.fe diff --git a/fec/tests/m9/okpair.fe b/fec/tests/generic/okpair.fe similarity index 100% rename from fec/tests/m9/okpair.fe rename to fec/tests/generic/okpair.fe diff --git a/fec/tests/m9/oksamrec.fe b/fec/tests/generic/oksamrec.fe similarity index 100% rename from fec/tests/m9/oksamrec.fe rename to fec/tests/generic/oksamrec.fe diff --git a/fec/tests/m9/okscope/lib.fe b/fec/tests/generic/okscope/lib.fe similarity index 100% rename from fec/tests/m9/okscope/lib.fe rename to fec/tests/generic/okscope/lib.fe diff --git a/fec/tests/m9/okscope/main.fe b/fec/tests/generic/okscope/main.fe similarity index 100% rename from fec/tests/m9/okscope/main.fe rename to fec/tests/generic/okscope/main.fe diff --git a/fec/tests/m9/okskip.fe b/fec/tests/generic/okskip.fe similarity index 100% rename from fec/tests/m9/okskip.fe rename to fec/tests/generic/okskip.fe diff --git a/fec/tests/m9/oktypeeq.fe b/fec/tests/generic/oktypeeq.fe similarity index 100% rename from fec/tests/m9/oktypeeq.fe rename to fec/tests/generic/oktypeeq.fe diff --git a/fec/tests/m7/README.md b/fec/tests/optional/README.md similarity index 100% rename from fec/tests/m7/README.md rename to fec/tests/optional/README.md diff --git a/fec/tests/m7/badcatch.fe b/fec/tests/optional/badcatch.fe similarity index 100% rename from fec/tests/m7/badcatch.fe rename to fec/tests/optional/badcatch.fe diff --git a/fec/tests/m7/baddef.fe b/fec/tests/optional/baddef.fe similarity index 100% rename from fec/tests/m7/baddef.fe rename to fec/tests/optional/baddef.fe diff --git a/fec/tests/m7/baddir.fe b/fec/tests/optional/baddir.fe similarity index 100% rename from fec/tests/m7/baddir.fe rename to fec/tests/optional/baddir.fe diff --git a/fec/tests/m7/badercod.fe b/fec/tests/optional/badercod.fe similarity index 100% rename from fec/tests/m7/badercod.fe rename to fec/tests/optional/badercod.fe diff --git a/fec/tests/m7/badernam.fe b/fec/tests/optional/badernam.fe similarity index 100% rename from fec/tests/m7/badernam.fe rename to fec/tests/optional/badernam.fe diff --git a/fec/tests/m7/badetype.fe b/fec/tests/optional/badetype.fe similarity index 100% rename from fec/tests/m7/badetype.fe rename to fec/tests/optional/badetype.fe diff --git a/fec/tests/m7/badnull.fe b/fec/tests/optional/badnull.fe similarity index 100% rename from fec/tests/m7/badnull.fe rename to fec/tests/optional/badnull.fe diff --git a/fec/tests/m7/badoref.fe b/fec/tests/optional/badoref.fe similarity index 100% rename from fec/tests/m7/badoref.fe rename to fec/tests/optional/badoref.fe diff --git a/fec/tests/m7/badorel.fe b/fec/tests/optional/badorel.fe similarity index 100% rename from fec/tests/m7/badorel.fe rename to fec/tests/optional/badorel.fe diff --git a/fec/tests/m7/badproj.fe b/fec/tests/optional/badproj.fe similarity index 100% rename from fec/tests/m7/badproj.fe rename to fec/tests/optional/badproj.fe diff --git a/fec/tests/m7/badqmark.fe b/fec/tests/optional/badqmark.fe similarity index 100% rename from fec/tests/m7/badqmark.fe rename to fec/tests/optional/badqmark.fe diff --git a/fec/tests/m7/badret.fe b/fec/tests/optional/badret.fe similarity index 100% rename from fec/tests/m7/badret.fe rename to fec/tests/optional/badret.fe diff --git a/fec/tests/m7/badsome.fe b/fec/tests/optional/badsome.fe similarity index 100% rename from fec/tests/m7/badsome.fe rename to fec/tests/optional/badsome.fe diff --git a/fec/tests/m7/badtry.fe b/fec/tests/optional/badtry.fe similarity index 100% rename from fec/tests/m7/badtry.fe rename to fec/tests/optional/badtry.fe diff --git a/fec/tests/m7/badzero.fe b/fec/tests/optional/badzero.fe similarity index 100% rename from fec/tests/m7/badzero.fe rename to fec/tests/optional/badzero.fe diff --git a/fec/tests/m7/okcatch.fe b/fec/tests/optional/okcatch.fe similarity index 100% rename from fec/tests/m7/okcatch.fe rename to fec/tests/optional/okcatch.fe diff --git a/fec/tests/m7/okcatmov.fe b/fec/tests/optional/okcatmov.fe similarity index 100% rename from fec/tests/m7/okcatmov.fe rename to fec/tests/optional/okcatmov.fe diff --git a/fec/tests/m7/okcvoid.fe b/fec/tests/optional/okcvoid.fe similarity index 100% rename from fec/tests/m7/okcvoid.fe rename to fec/tests/optional/okcvoid.fe diff --git a/fec/tests/m7/okdeflt.fe b/fec/tests/optional/okdeflt.fe similarity index 100% rename from fec/tests/m7/okdeflt.fe rename to fec/tests/optional/okdeflt.fe diff --git a/fec/tests/m7/okiflet.fe b/fec/tests/optional/okiflet.fe similarity index 100% rename from fec/tests/m7/okiflet.fe rename to fec/tests/optional/okiflet.fe diff --git a/fec/tests/m7/okmatch.fe b/fec/tests/optional/okmatch.fe similarity index 100% rename from fec/tests/m7/okmatch.fe rename to fec/tests/optional/okmatch.fe diff --git a/fec/tests/m7/oknull.fe b/fec/tests/optional/oknull.fe similarity index 100% rename from fec/tests/m7/oknull.fe rename to fec/tests/optional/oknull.fe diff --git a/fec/tests/m7/okorelse.fe b/fec/tests/optional/okorelse.fe similarity index 100% rename from fec/tests/m7/okorelse.fe rename to fec/tests/optional/okorelse.fe diff --git a/fec/tests/m7/okpatvw.fe b/fec/tests/optional/okpatvw.fe similarity index 100% rename from fec/tests/m7/okpatvw.fe rename to fec/tests/optional/okpatvw.fe diff --git a/fec/tests/m7/okproj.fe b/fec/tests/optional/okproj.fe similarity index 100% rename from fec/tests/m7/okproj.fe rename to fec/tests/optional/okproj.fe diff --git a/fec/tests/m7/okrepl.fe b/fec/tests/optional/okrepl.fe similarity index 100% rename from fec/tests/m7/okrepl.fe rename to fec/tests/optional/okrepl.fe diff --git a/fec/tests/m7/oktrdef.fe b/fec/tests/optional/oktrdef.fe similarity index 100% rename from fec/tests/m7/oktrdef.fe rename to fec/tests/optional/oktrdef.fe diff --git a/fec/tests/m7/oktry.fe b/fec/tests/optional/oktry.fe similarity index 100% rename from fec/tests/m7/oktry.fe rename to fec/tests/optional/oktry.fe diff --git a/fec/tests/m6/README.md b/fec/tests/own/README.md similarity index 100% rename from fec/tests/m6/README.md rename to fec/tests/own/README.md diff --git a/fec/tests/m6/badarg.fe b/fec/tests/own/badarg.fe similarity index 100% rename from fec/tests/m6/badarg.fe rename to fec/tests/own/badarg.fe diff --git a/fec/tests/m6/badbinit.fe b/fec/tests/own/badbinit.fe similarity index 100% rename from fec/tests/m6/badbinit.fe rename to fec/tests/own/badbinit.fe diff --git a/fec/tests/m6/badbrmov.fe b/fec/tests/own/badbrmov.fe similarity index 100% rename from fec/tests/m6/badbrmov.fe rename to fec/tests/own/badbrmov.fe diff --git a/fec/tests/m6/baddefer.fe b/fec/tests/own/baddefer.fe similarity index 100% rename from fec/tests/m6/baddefer.fe rename to fec/tests/own/baddefer.fe diff --git a/fec/tests/m6/badfld.fe b/fec/tests/own/badfld.fe similarity index 100% rename from fec/tests/m6/badfld.fe rename to fec/tests/own/badfld.fe diff --git a/fec/tests/m6/badglob.fe b/fec/tests/own/badglob.fe similarity index 100% rename from fec/tests/m6/badglob.fe rename to fec/tests/own/badglob.fe diff --git a/fec/tests/m6/badgmut.fe b/fec/tests/own/badgmut.fe similarity index 100% rename from fec/tests/m6/badgmut.fe rename to fec/tests/own/badgmut.fe diff --git a/fec/tests/m6/badinv.fe b/fec/tests/own/badinv.fe similarity index 100% rename from fec/tests/m6/badinv.fe rename to fec/tests/own/badinv.fe diff --git a/fec/tests/m6/badlocsl.fe b/fec/tests/own/badlocsl.fe similarity index 100% rename from fec/tests/m6/badlocsl.fe rename to fec/tests/own/badlocsl.fe diff --git a/fec/tests/m6/badloop.fe b/fec/tests/own/badloop.fe similarity index 100% rename from fec/tests/m6/badloop.fe rename to fec/tests/own/badloop.fe diff --git a/fec/tests/m6/badmove.fe b/fec/tests/own/badmove.fe similarity index 100% rename from fec/tests/m6/badmove.fe rename to fec/tests/own/badmove.fe diff --git a/fec/tests/m6/badmut.fe b/fec/tests/own/badmut.fe similarity index 100% rename from fec/tests/m6/badmut.fe rename to fec/tests/own/badmut.fe diff --git a/fec/tests/m6/badmut2.fe b/fec/tests/own/badmut2.fe similarity index 100% rename from fec/tests/m6/badmut2.fe rename to fec/tests/own/badmut2.fe diff --git a/fec/tests/m6/badptr.fe b/fec/tests/own/badptr.fe similarity index 100% rename from fec/tests/m6/badptr.fe rename to fec/tests/own/badptr.fe diff --git a/fec/tests/m6/badret.fe b/fec/tests/own/badret.fe similarity index 100% rename from fec/tests/m6/badret.fe rename to fec/tests/own/badret.fe diff --git a/fec/tests/m6/badrfld.fe b/fec/tests/own/badrfld.fe similarity index 100% rename from fec/tests/m6/badrfld.fe rename to fec/tests/own/badrfld.fe diff --git a/fec/tests/m6/badridx.fe b/fec/tests/own/badridx.fe similarity index 100% rename from fec/tests/m6/badridx.fe rename to fec/tests/own/badridx.fe diff --git a/fec/tests/m6/badscop.fe b/fec/tests/own/badscop.fe similarity index 100% rename from fec/tests/m6/badscop.fe rename to fec/tests/own/badscop.fe diff --git a/fec/tests/m6/badself.fe b/fec/tests/own/badself.fe similarity index 100% rename from fec/tests/m6/badself.fe rename to fec/tests/own/badself.fe diff --git a/fec/tests/m6/badshwr.fe b/fec/tests/own/badshwr.fe similarity index 100% rename from fec/tests/m6/badshwr.fe rename to fec/tests/own/badshwr.fe diff --git a/fec/tests/m6/badslfld.fe b/fec/tests/own/badslfld.fe similarity index 100% rename from fec/tests/m6/badslfld.fe rename to fec/tests/own/badslfld.fe diff --git a/fec/tests/m6/badtwo.fe b/fec/tests/own/badtwo.fe similarity index 100% rename from fec/tests/m6/badtwo.fe rename to fec/tests/own/badtwo.fe diff --git a/fec/tests/m6/badup.fe b/fec/tests/own/badup.fe similarity index 100% rename from fec/tests/m6/badup.fe rename to fec/tests/own/badup.fe diff --git a/fec/tests/m6/badweak.fe b/fec/tests/own/badweak.fe similarity index 100% rename from fec/tests/m6/badweak.fe rename to fec/tests/own/badweak.fe diff --git a/fec/tests/m5/defer.fe b/fec/tests/own/ok-defer.fe similarity index 100% rename from fec/tests/m5/defer.fe rename to fec/tests/own/ok-defer.fe diff --git a/fec/tests/m5/owned.fe b/fec/tests/own/ok-owned.fe similarity index 100% rename from fec/tests/m5/owned.fe rename to fec/tests/own/ok-owned.fe diff --git a/fec/tests/m6/okbranch.fe b/fec/tests/own/okbranch.fe similarity index 100% rename from fec/tests/m6/okbranch.fe rename to fec/tests/own/okbranch.fe diff --git a/fec/tests/m6/okdefer.fe b/fec/tests/own/okdefer.fe similarity index 100% rename from fec/tests/m6/okdefer.fe rename to fec/tests/own/okdefer.fe diff --git a/fec/tests/m6/okglobcp.fe b/fec/tests/own/okglobcp.fe similarity index 100% rename from fec/tests/m6/okglobcp.fe rename to fec/tests/own/okglobcp.fe diff --git a/fec/tests/m6/oklast.fe b/fec/tests/own/oklast.fe similarity index 100% rename from fec/tests/m6/oklast.fe rename to fec/tests/own/oklast.fe diff --git a/fec/tests/m6/okr8free.fe b/fec/tests/own/okr8free.fe similarity index 100% rename from fec/tests/m6/okr8free.fe rename to fec/tests/own/okr8free.fe diff --git a/fec/tests/m6/okr8join.fe b/fec/tests/own/okr8join.fe similarity index 100% rename from fec/tests/m6/okr8join.fe rename to fec/tests/own/okr8join.fe diff --git a/fec/tests/m6/okr8meth.fe b/fec/tests/own/okr8meth.fe similarity index 100% rename from fec/tests/m6/okr8meth.fe rename to fec/tests/own/okr8meth.fe diff --git a/fec/tests/m6/okr8stat.fe b/fec/tests/own/okr8stat.fe similarity index 100% rename from fec/tests/m6/okr8stat.fe rename to fec/tests/own/okr8stat.fe diff --git a/fec/tests/m6/okrebor.fe b/fec/tests/own/okrebor.fe similarity index 100% rename from fec/tests/m6/okrebor.fe rename to fec/tests/own/okrebor.fe diff --git a/fec/tests/m6/okrtlast.fe b/fec/tests/own/okrtlast.fe similarity index 100% rename from fec/tests/m6/okrtlast.fe rename to fec/tests/own/okrtlast.fe diff --git a/fec/tests/m6/okshare.fe b/fec/tests/own/okshare.fe similarity index 100% rename from fec/tests/m6/okshare.fe rename to fec/tests/own/okshare.fe diff --git a/fec/tests/m6/okslreb.fe b/fec/tests/own/okslreb.fe similarity index 100% rename from fec/tests/m6/okslreb.fe rename to fec/tests/own/okslreb.fe diff --git a/fec/tests/m6/okstatic.fe b/fec/tests/own/okstatic.fe similarity index 100% rename from fec/tests/m6/okstatic.fe rename to fec/tests/own/okstatic.fe diff --git a/fec/tests/m6/oktemp.fe b/fec/tests/own/oktemp.fe similarity index 100% rename from fec/tests/m6/oktemp.fe rename to fec/tests/own/oktemp.fe diff --git a/fec/tests/m6/oktrim.fe b/fec/tests/own/oktrim.fe similarity index 100% rename from fec/tests/m6/oktrim.fe rename to fec/tests/own/oktrim.fe diff --git a/fec/tests/m6/okwcall.fe b/fec/tests/own/okwcall.fe similarity index 100% rename from fec/tests/m6/okwcall.fe rename to fec/tests/own/okwcall.fe diff --git a/fec/tests/m5/bad-clos.fe b/fec/tests/own/own-bad-clos.fe similarity index 100% rename from fec/tests/m5/bad-clos.fe rename to fec/tests/own/own-bad-clos.fe diff --git a/fec/tests/m5/bad-cond.fe b/fec/tests/own/own-bad-cond.fe similarity index 100% rename from fec/tests/m5/bad-cond.fe rename to fec/tests/own/own-bad-cond.fe diff --git a/fec/tests/m5/bad-dbl.fe b/fec/tests/own/own-bad-dbl.fe similarity index 100% rename from fec/tests/m5/bad-dbl.fe rename to fec/tests/own/own-bad-dbl.fe diff --git a/fec/tests/m5/bad-dest.fe b/fec/tests/own/own-bad-dest.fe similarity index 100% rename from fec/tests/m5/bad-dest.fe rename to fec/tests/own/own-bad-dest.fe diff --git a/fec/tests/m5/bad-drop.fe b/fec/tests/own/own-bad-drop.fe similarity index 100% rename from fec/tests/m5/bad-drop.fe rename to fec/tests/own/own-bad-drop.fe diff --git a/fec/tests/m5/bad-loop.fe b/fec/tests/own/own-bad-loop.fe similarity index 100% rename from fec/tests/m5/bad-loop.fe rename to fec/tests/own/own-bad-loop.fe diff --git a/fec/tests/m5/bad-move.fe b/fec/tests/own/own-bad-move.fe similarity index 100% rename from fec/tests/m5/bad-move.fe rename to fec/tests/own/own-bad-move.fe diff --git a/fec/tests/m5/bad-proj.fe b/fec/tests/own/own-bad-proj.fe similarity index 100% rename from fec/tests/m5/bad-proj.fe rename to fec/tests/own/own-bad-proj.fe diff --git a/fec/tests/pass/basic.fe b/fec/tests/parse/basic.fe similarity index 100% rename from fec/tests/pass/basic.fe rename to fec/tests/parse/basic.fe diff --git a/fec/tests/pass/keybuilt.fe b/fec/tests/parse/keybuilt.fe similarity index 100% rename from fec/tests/pass/keybuilt.fe rename to fec/tests/parse/keybuilt.fe diff --git a/fec/tests/pass/literals.fe b/fec/tests/parse/literals.fe similarity index 100% rename from fec/tests/pass/literals.fe rename to fec/tests/parse/literals.fe diff --git a/fec/tests/fail/logical.fe b/fec/tests/parse/logical.fe similarity index 100% rename from fec/tests/fail/logical.fe rename to fec/tests/parse/logical.fe diff --git a/fec/tests/fail/misssemi.fe b/fec/tests/parse/misssemi.fe similarity index 100% rename from fec/tests/fail/misssemi.fe rename to fec/tests/parse/misssemi.fe diff --git a/fec/tests/fail/unclcomm.fe b/fec/tests/parse/unclcomm.fe similarity index 100% rename from fec/tests/fail/unclcomm.fe rename to fec/tests/parse/unclcomm.fe diff --git a/fec/tests/pass/v012form.fe b/fec/tests/parse/v012form.fe similarity index 100% rename from fec/tests/pass/v012form.fe rename to fec/tests/parse/v012form.fe diff --git a/fec/tests/m3/nochk.fe b/fec/tests/pending-backend/bounds-nocheck.fe similarity index 100% rename from fec/tests/m3/nochk.fe rename to fec/tests/pending-backend/bounds-nocheck.fe diff --git a/fec/tests/m3/bounds.fe b/fec/tests/pending-backend/bounds-trap.fe similarity index 100% rename from fec/tests/m3/bounds.fe rename to fec/tests/pending-backend/bounds-trap.fe diff --git a/fec/tests/m4/proptest.c b/fec/tests/pending-backend/format-prop.c similarity index 100% rename from fec/tests/m4/proptest.c rename to fec/tests/pending-backend/format-prop.c diff --git a/fec/tests/m5/runtime.c b/fec/tests/pending-backend/ownership-drop.c similarity index 100% rename from fec/tests/m5/runtime.c rename to fec/tests/pending-backend/ownership-drop.c diff --git a/fec/tests/m5/runtime.fe b/fec/tests/pending-backend/ownership-drop.fe similarity index 100% rename from fec/tests/m5/runtime.fe rename to fec/tests/pending-backend/ownership-drop.fe diff --git a/fec/tests/m3/slcbound.fe b/fec/tests/pending-backend/slice-bounds-trap.fe similarity index 100% rename from fec/tests/m3/slcbound.fe rename to fec/tests/pending-backend/slice-bounds-trap.fe diff --git a/fec/tests/m2/bad-ari.fe b/fec/tests/types/bad-ari.fe similarity index 100% rename from fec/tests/m2/bad-ari.fe rename to fec/tests/types/bad-ari.fe diff --git a/fec/tests/m2/bad-asgn.fe b/fec/tests/types/bad-asgn.fe similarity index 100% rename from fec/tests/m2/bad-asgn.fe rename to fec/tests/types/bad-asgn.fe diff --git a/fec/tests/m2/bad-cast.fe b/fec/tests/types/bad-cast.fe similarity index 100% rename from fec/tests/m2/bad-cast.fe rename to fec/tests/types/bad-cast.fe diff --git a/fec/tests/m2/bad-cond.fe b/fec/tests/types/bad-cond.fe similarity index 100% rename from fec/tests/m2/bad-cond.fe rename to fec/tests/types/bad-cond.fe diff --git a/fec/tests/m3/bad-mlet.fe b/fec/tests/types/bad-mlet.fe similarity index 100% rename from fec/tests/m3/bad-mlet.fe rename to fec/tests/types/bad-mlet.fe diff --git a/fec/tests/m2/bad-ret.fe b/fec/tests/types/bad-ret.fe similarity index 100% rename from fec/tests/m2/bad-ret.fe rename to fec/tests/types/bad-ret.fe diff --git a/fec/tests/m3/bad-shwr.fe b/fec/tests/types/bad-shwr.fe similarity index 100% rename from fec/tests/m3/bad-shwr.fe rename to fec/tests/types/bad-shwr.fe diff --git a/fec/tests/m2/bad-type.fe b/fec/tests/types/bad-type.fe similarity index 100% rename from fec/tests/m2/bad-type.fe rename to fec/tests/types/bad-type.fe diff --git a/fec/tests/m2/bad-unit.fe b/fec/tests/types/bad-unit.fe similarity index 100% rename from fec/tests/m2/bad-unit.fe rename to fec/tests/types/bad-unit.fe diff --git a/fec/tests/m2/bad-unk.fe b/fec/tests/types/bad-unk.fe similarity index 100% rename from fec/tests/m2/bad-unk.fe rename to fec/tests/types/bad-unk.fe diff --git a/fec/tests/m2/bad-void.fe b/fec/tests/types/bad-void.fe similarity index 100% rename from fec/tests/m2/bad-void.fe rename to fec/tests/types/bad-void.fe diff --git a/fec/tests/m3/badarr.fe b/fec/tests/types/badarr.fe similarity index 100% rename from fec/tests/m3/badarr.fe rename to fec/tests/types/badarr.fe diff --git a/fec/tests/m3/badchar.fe b/fec/tests/types/badchar.fe similarity index 100% rename from fec/tests/m3/badchar.fe rename to fec/tests/types/badchar.fe diff --git a/fec/tests/m3/badcycle.fe b/fec/tests/types/badcycle.fe similarity index 100% rename from fec/tests/m3/badcycle.fe rename to fec/tests/types/badcycle.fe diff --git a/fec/tests/m3/badfield.fe b/fec/tests/types/badfield.fe similarity index 100% rename from fec/tests/m3/badfield.fe rename to fec/tests/types/badfield.fe diff --git a/fec/tests/m3/badfld.fe b/fec/tests/types/badfld.fe similarity index 100% rename from fec/tests/m3/badfld.fe rename to fec/tests/types/badfld.fe diff --git a/fec/tests/m3/badindex.fe b/fec/tests/types/badindex.fe similarity index 100% rename from fec/tests/m3/badindex.fe rename to fec/tests/types/badindex.fe diff --git a/fec/tests/m3/badmat.fe b/fec/tests/types/badmat.fe similarity index 100% rename from fec/tests/m3/badmat.fe rename to fec/tests/types/badmat.fe diff --git a/fec/tests/m3/badstr.fe b/fec/tests/types/badstr.fe similarity index 100% rename from fec/tests/m3/badstr.fe rename to fec/tests/types/badstr.fe diff --git a/fec/tests/m3/array.fe b/fec/tests/types/ok-array.fe similarity index 100% rename from fec/tests/m3/array.fe rename to fec/tests/types/ok-array.fe diff --git a/fec/tests/m3/arrayctx.fe b/fec/tests/types/ok-arrayctx.fe similarity index 100% rename from fec/tests/m3/arrayctx.fe rename to fec/tests/types/ok-arrayctx.fe diff --git a/fec/tests/m2/castwhil.fe b/fec/tests/types/ok-castwhil.fe similarity index 100% rename from fec/tests/m2/castwhil.fe rename to fec/tests/types/ok-castwhil.fe diff --git a/fec/tests/m3/char.fe b/fec/tests/types/ok-char.fe similarity index 100% rename from fec/tests/m3/char.fe rename to fec/tests/types/ok-char.fe diff --git a/fec/tests/m3/enum.fe b/fec/tests/types/ok-enum.fe similarity index 100% rename from fec/tests/m3/enum.fe rename to fec/tests/types/ok-enum.fe diff --git a/fec/tests/m3/for.fe b/fec/tests/types/ok-for.fe similarity index 100% rename from fec/tests/m3/for.fe rename to fec/tests/types/ok-for.fe diff --git a/fec/tests/m2/hello.fe b/fec/tests/types/ok-hello.fe similarity index 100% rename from fec/tests/m2/hello.fe rename to fec/tests/types/ok-hello.fe diff --git a/fec/tests/m3/mutable.fe b/fec/tests/types/ok-mutable.fe similarity index 100% rename from fec/tests/m3/mutable.fe rename to fec/tests/types/ok-mutable.fe diff --git a/fec/tests/m3/nested.fe b/fec/tests/types/ok-nested.fe similarity index 100% rename from fec/tests/m3/nested.fe rename to fec/tests/types/ok-nested.fe diff --git a/fec/tests/m2/scopes.fe b/fec/tests/types/ok-scopes.fe similarity index 100% rename from fec/tests/m2/scopes.fe rename to fec/tests/types/ok-scopes.fe diff --git a/fec/tests/m3/str.fe b/fec/tests/types/ok-str.fe similarity index 100% rename from fec/tests/m3/str.fe rename to fec/tests/types/ok-str.fe diff --git a/fec/tests/m3/struct.fe b/fec/tests/types/ok-struct.fe similarity index 100% rename from fec/tests/m3/struct.fe rename to fec/tests/types/ok-struct.fe diff --git a/fec/tests/m8/README.md b/fec/tests/units/README.md similarity index 100% rename from fec/tests/m8/README.md rename to fec/tests/units/README.md diff --git a/fec/tests/m8/alias/acme/math.fe b/fec/tests/units/alias/acme/math.fe similarity index 100% rename from fec/tests/m8/alias/acme/math.fe rename to fec/tests/units/alias/acme/math.fe diff --git a/fec/tests/m8/alias/main.fe b/fec/tests/units/alias/main.fe similarity index 100% rename from fec/tests/m8/alias/main.fe rename to fec/tests/units/alias/main.fe diff --git a/fec/tests/m8/badlong/main.fe b/fec/tests/units/badlong/main.fe similarity index 100% rename from fec/tests/m8/badlong/main.fe rename to fec/tests/units/badlong/main.fe diff --git a/fec/tests/m8/badupper/main.fe b/fec/tests/units/badupper/main.fe similarity index 100% rename from fec/tests/m8/badupper/main.fe rename to fec/tests/units/badupper/main.fe diff --git a/fec/tests/m8/basic/main.fe b/fec/tests/units/basic/main.fe similarity index 100% rename from fec/tests/m8/basic/main.fe rename to fec/tests/units/basic/main.fe diff --git a/fec/tests/m8/basic/util.fe b/fec/tests/units/basic/util.fe similarity index 100% rename from fec/tests/m8/basic/util.fe rename to fec/tests/units/basic/util.fe diff --git a/fec/tests/m8/bindconf/alpha/net.fe b/fec/tests/units/bindconf/alpha/net.fe similarity index 100% rename from fec/tests/m8/bindconf/alpha/net.fe rename to fec/tests/units/bindconf/alpha/net.fe diff --git a/fec/tests/m8/bindconf/beta/net.fe b/fec/tests/units/bindconf/beta/net.fe similarity index 100% rename from fec/tests/m8/bindconf/beta/net.fe rename to fec/tests/units/bindconf/beta/net.fe diff --git a/fec/tests/m8/bindconf/main.fe b/fec/tests/units/bindconf/main.fe similarity index 100% rename from fec/tests/m8/bindconf/main.fe rename to fec/tests/units/bindconf/main.fe diff --git a/fec/tests/m8/cycle/a.fe b/fec/tests/units/cycle/a.fe similarity index 100% rename from fec/tests/m8/cycle/a.fe rename to fec/tests/units/cycle/a.fe diff --git a/fec/tests/m8/cycle/b.fe b/fec/tests/units/cycle/b.fe similarity index 100% rename from fec/tests/m8/cycle/b.fe rename to fec/tests/units/cycle/b.fe diff --git a/fec/tests/m8/dotpriv/game/bar.fe b/fec/tests/units/dotpriv/game/bar.fe similarity index 100% rename from fec/tests/m8/dotpriv/game/bar.fe rename to fec/tests/units/dotpriv/game/bar.fe diff --git a/fec/tests/m8/dotpriv/game/foo.fe b/fec/tests/units/dotpriv/game/foo.fe similarity index 100% rename from fec/tests/m8/dotpriv/game/foo.fe rename to fec/tests/units/dotpriv/game/foo.fe diff --git a/fec/tests/m8/dotpriv/main.fe b/fec/tests/units/dotpriv/main.fe similarity index 100% rename from fec/tests/m8/dotpriv/main.fe rename to fec/tests/units/dotpriv/main.fe diff --git a/fec/tests/m8/dotted/acme/math.fe b/fec/tests/units/dotted/acme/math.fe similarity index 100% rename from fec/tests/m8/dotted/acme/math.fe rename to fec/tests/units/dotted/acme/math.fe diff --git a/fec/tests/m8/dotted/main.fe b/fec/tests/units/dotted/main.fe similarity index 100% rename from fec/tests/m8/dotted/main.fe rename to fec/tests/units/dotted/main.fe diff --git a/fec/tests/m8/errdet/alpha.fe b/fec/tests/units/errdet/alpha.fe similarity index 100% rename from fec/tests/m8/errdet/alpha.fe rename to fec/tests/units/errdet/alpha.fe diff --git a/fec/tests/m8/errdet/beta.fe b/fec/tests/units/errdet/beta.fe similarity index 100% rename from fec/tests/m8/errdet/beta.fe rename to fec/tests/units/errdet/beta.fe diff --git a/fec/tests/m8/errdet/main.fe b/fec/tests/units/errdet/main.fe similarity index 100% rename from fec/tests/m8/errdet/main.fe rename to fec/tests/units/errdet/main.fe diff --git a/fec/tests/m8/errnom/lib.fe b/fec/tests/units/errnom/lib.fe similarity index 100% rename from fec/tests/m8/errnom/lib.fe rename to fec/tests/units/errnom/lib.fe diff --git a/fec/tests/m8/errnom/main.fe b/fec/tests/units/errnom/main.fe similarity index 100% rename from fec/tests/m8/errnom/main.fe rename to fec/tests/units/errnom/main.fe diff --git a/fec/tests/m8/errsame/alpha.fe b/fec/tests/units/errsame/alpha.fe similarity index 100% rename from fec/tests/m8/errsame/alpha.fe rename to fec/tests/units/errsame/alpha.fe diff --git a/fec/tests/m8/errsame/beta.fe b/fec/tests/units/errsame/beta.fe similarity index 100% rename from fec/tests/m8/errsame/beta.fe rename to fec/tests/units/errsame/beta.fe diff --git a/fec/tests/m8/errsame/main.fe b/fec/tests/units/errsame/main.fe similarity index 100% rename from fec/tests/m8/errsame/main.fe rename to fec/tests/units/errsame/main.fe diff --git a/fec/tests/m8/missing/main.fe b/fec/tests/units/missing/main.fe similarity index 100% rename from fec/tests/m8/missing/main.fe rename to fec/tests/units/missing/main.fe diff --git a/fec/tests/m8/privfld/data.fe b/fec/tests/units/privfld/data.fe similarity index 100% rename from fec/tests/m8/privfld/data.fe rename to fec/tests/units/privfld/data.fe diff --git a/fec/tests/m8/privfld/main.fe b/fec/tests/units/privfld/main.fe similarity index 100% rename from fec/tests/m8/privfld/main.fe rename to fec/tests/units/privfld/main.fe diff --git a/fec/tests/m8/privfn/main.fe b/fec/tests/units/privfn/main.fe similarity index 100% rename from fec/tests/m8/privfn/main.fe rename to fec/tests/units/privfn/main.fe diff --git a/fec/tests/m8/privfn/util.fe b/fec/tests/units/privfn/util.fe similarity index 100% rename from fec/tests/m8/privfn/util.fe rename to fec/tests/units/privfn/util.fe diff --git a/fec/tests/m8/pubfld/data.fe b/fec/tests/units/pubfld/data.fe similarity index 100% rename from fec/tests/m8/pubfld/data.fe rename to fec/tests/units/pubfld/data.fe diff --git a/fec/tests/m8/pubfld/main.fe b/fec/tests/units/pubfld/main.fe similarity index 100% rename from fec/tests/m8/pubfld/main.fe rename to fec/tests/units/pubfld/main.fe diff --git a/fec/tests/m8/pubpriv/lib.fe b/fec/tests/units/pubpriv/lib.fe similarity index 100% rename from fec/tests/m8/pubpriv/lib.fe rename to fec/tests/units/pubpriv/lib.fe diff --git a/fec/tests/m8/pubpriv/main.fe b/fec/tests/units/pubpriv/main.fe similarity index 100% rename from fec/tests/m8/pubpriv/main.fe rename to fec/tests/units/pubpriv/main.fe diff --git a/fec/tests/m8/unitbad/main.fe b/fec/tests/units/unitbad/main.fe similarity index 100% rename from fec/tests/m8/unitbad/main.fe rename to fec/tests/units/unitbad/main.fe diff --git a/src/ferrolang_vm/__init__.py b/src/ferrolang_vm/__init__.py deleted file mode 100644 index a63f890..0000000 --- a/src/ferrolang_vm/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Reproducible DOS development tools for the Ferro compiler.""" diff --git a/src/ferrolang_vm/dos_cli.py b/src/ferrolang_vm/dos_cli.py deleted file mode 100644 index 02a0567..0000000 --- a/src/ferrolang_vm/dos_cli.py +++ /dev/null @@ -1,80 +0,0 @@ -"""General-purpose disposable DOSBox-X/Open Watcom environment CLI.""" -from __future__ import annotations - -import argparse -import subprocess -import sys -from pathlib import Path - -from .dosboxx import DosboxError, run_suite, setup -from .paths import ROOT -from .suite import Case - - -def _run(command: str | None, *, keep: bool, show_dos: bool) -> int: - cases = [] if command is None else [Case("command", 0, command, True)] - run = run_suite(cases, keep=keep, show_dos=show_dos, trace_dos=show_dos) - try: - if run.result() != "PASS": - print(run.log(), file=sys.stderr) - return 1 - if cases and run.result(cases[0]) != "PASS": - print(run.log(cases[0]), file=sys.stderr) - return 1 - if cases: - output = run.log(cases[0]) - if output: - print(output, end="" if output.endswith("\n") else "\n") - if keep: - print(f"DOS workspace: {run.root}") - return 0 - finally: - run.cleanup() - - -def main() -> int: - parser = argparse.ArgumentParser( - prog="ferro-dos", - description="Disposable directory-backed DOSBox-X/Open Watcom environment.", - ) - commands = parser.add_subparsers(dest="action", required=True) - prepare = commands.add_parser("setup", help="install the pinned DOSBox-X and Open Watcom tools") - prepare.add_argument("--accept-watcom-license", action="store_true") - for name, help_text in ( - ("build", "build the current FEC source inside DOS"), - ("exec", "build FEC and execute one DOS command"), - ("batch", "build FEC and call a repository DOS batch"), - ("shell", "build FEC and open an interactive DOS shell"), - ): - command = commands.add_parser(name, help=help_text) - command.add_argument("--keep", action="store_true", help="preserve the temporary DOS workspace") - command.add_argument("--show-dos", action="store_true", help="show and pause the DOS window") - if name == "exec": - command.add_argument("dos_command") - elif name == "batch": - command.add_argument("path", type=Path) - args = parser.parse_args() - try: - if args.action == "setup": - dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) - print(f"DOSBox-X: {dosbox}") - print(f"Open Watcom: {watcom}") - return 0 - if args.action == "build": - return _run(None, keep=args.keep, show_dos=args.show_dos) - if args.action == "exec": - return _run(args.dos_command, keep=args.keep, show_dos=args.show_dos) - if args.action == "shell": - return _run("COMMAND.COM", keep=args.keep, show_dos=True) - path = (ROOT / args.path).resolve() - if (path != ROOT and ROOT not in path.parents) or not path.is_file(): - raise DosboxError("batch path must be an existing file inside the repository") - relative = path.relative_to(ROOT).as_posix().replace("/", "\\").upper() - return _run(f"CALL R:\\{relative}", keep=args.keep, show_dos=args.show_dos) - except (DosboxError, subprocess.SubprocessError) as exc: - print(f"ferro-dos: {exc}", file=sys.stderr) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/ferrolang_vm/dosboxx.py b/src/ferrolang_vm/dosboxx.py deleted file mode 100644 index 0dcc2b3..0000000 --- a/src/ferrolang_vm/dosboxx.py +++ /dev/null @@ -1,332 +0,0 @@ -"""Reproducible DOSBox-X/Open Watcom development test backend.""" -from __future__ import annotations - -import hashlib -import json -import os -import shutil -import subprocess -import tempfile -import urllib.request -import zipfile -from dataclasses import dataclass -from pathlib import Path - -from .paths import ROOT -from .suite import Case - - -CACHE = ROOT / ".dosboxx" -LOCK_PATH = ROOT / "tools" / "toolchains" / "dosboxx.lock.json" -RUNS = CACHE / "runs" - - -class DosboxError(RuntimeError): - pass - - -def _lock() -> dict[str, object]: - return json.loads(LOCK_PATH.read_text(encoding="utf-8")) - - -def _sha256(path: Path) -> str: - digest = hashlib.sha256() - with path.open("rb") as source: - while chunk := source.read(1024 * 1024): - digest.update(chunk) - return digest.hexdigest() - - -def _download(name: str, spec: dict[str, object]) -> Path: - downloads = CACHE / "downloads" - downloads.mkdir(parents=True, exist_ok=True) - target = downloads / Path(str(spec["url"])).name - expected = str(spec["sha256"]).lower() - if target.is_file() and _sha256(target) == expected: - return target - target.unlink(missing_ok=True) - partial = target.with_suffix(target.suffix + ".part") - partial.unlink(missing_ok=True) - print(f"ferro-test: downloading {name} {spec['version']}...") - try: - with urllib.request.urlopen(str(spec["url"])) as response, partial.open("wb") as output: - shutil.copyfileobj(response, output, length=1024 * 1024) - except Exception: - partial.unlink(missing_ok=True) - raise - actual = _sha256(partial) - if actual != expected: - partial.unlink(missing_ok=True) - raise DosboxError(f"{name} SHA-256 mismatch: expected {expected}, got {actual}") - partial.replace(target) - return target - - -def _safe_extract(archive: Path, destination: Path) -> None: - destination.parent.mkdir(parents=True, exist_ok=True) - temporary = Path(tempfile.mkdtemp(prefix=f".{destination.name}-", dir=destination.parent)) - try: - with zipfile.ZipFile(archive) as bundle: - root = temporary.resolve() - for member in bundle.infolist(): - target = (temporary / member.filename).resolve() - if target != root and root not in target.parents: - raise DosboxError(f"unsafe archive member: {member.filename}") - bundle.extractall(temporary) - if destination.exists(): - shutil.rmtree(destination) - temporary.replace(destination) - except Exception: - shutil.rmtree(temporary, ignore_errors=True) - raise - - -def _required_paths(lock: dict[str, object]) -> tuple[Path, Path, list[Path]]: - dosbox_spec = lock["dosboxx"] - watcom_spec = lock["open_watcom"] - assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) - dosbox = CACHE / "dosbox-x" / str(dosbox_spec["executable"]) - watcom = CACHE / "watcom" - required = [watcom / str(item) for item in watcom_spec["required"]] - return dosbox, watcom, required - - -def setup(*, accept_watcom_license: bool = False) -> tuple[Path, Path]: - if os.name != "nt": - raise DosboxError("the DOSBox-X development backend currently supports Windows only") - lock = _lock() - dosbox, watcom, watcom_required = _required_paths(lock) - tools_ready = dosbox.is_file() and all(path.is_file() for path in watcom_required) - if not tools_ready and not accept_watcom_license: - raise DosboxError( - "Open Watcom is distributed under the Sybase Open Watcom Public License. " - "Review tools/toolchains/dosboxx.lock.json and rerun " - "`uv run ferro-dos setup --accept-watcom-license`." - ) - if not tools_ready: - dosbox_spec = lock["dosboxx"] - watcom_spec = lock["open_watcom"] - assert isinstance(dosbox_spec, dict) and isinstance(watcom_spec, dict) - _safe_extract(_download("DOSBox-X", dosbox_spec), CACHE / "dosbox-x") - _safe_extract(_download("Open Watcom", watcom_spec), watcom) - dosbox, watcom, watcom_required = _required_paths(lock) - missing = [str(path.relative_to(CACHE)) for path in [dosbox, *watcom_required] - if not path.is_file()] - if missing: - raise DosboxError("toolchain archive is missing: " + ", ".join(missing)) - (CACHE / "SETUP.OK").write_text(_sha256(LOCK_PATH) + "\n", encoding="ascii") - return dosbox, watcom - - -def resolve_tools() -> tuple[Path, Path]: - lock = _lock() - dosbox, watcom, required = _required_paths(lock) - if not dosbox.is_file() or not all(path.is_file() for path in required): - raise DosboxError("toolchain is not installed; run `uv run ferro-dos setup`") - return dosbox, watcom - - -# ``if errorlevel N`` in DOS tests ``>= N``, so an exact code needs a descending -# ladder. Small values get their own rung because they carry the meaning -- 1 is -# an ordinary compiler error, 3 is Watcom's abort() -- while anything above 8 is -# bucketed to a lower bound, which is enough to tell a crash from a diagnostic. -_RC_LADDER = (255, 128, 64, 32, 16, 8, 7, 6, 5, 4, 3, 2, 1) - - -def _rc_batch() -> str: - """Batch helper that records the previous command's exit code. - - Called as ``call RC.BAT `` right after a case command, since anything - else -- including writing a file -- would clobber ERRORLEVEL first. Note the - space before each ``>``: ``echo 0>FILE`` would parse as a redirect of handle - 0 rather than an echo of "0", so the value is written with a trailing space - and stripped on the host. - """ - lines = ["@echo off"] - lines.extend(f"if errorlevel {value} goto R{value}" for value in _RC_LADDER) - lines.extend(["echo 0 >RESULTS\\%1.RC", "goto END"]) - for value in _RC_LADDER: - lines.extend([f":R{value}", f"echo {value} >RESULTS\\%1.RC", "goto END"]) - lines.extend([":END", ""]) - return "\r\n".join(lines) - - -def _batch(cases: list[Case], *, show_dos: bool, trace_dos: bool, - prebuilt: bool = False) -> str: - # The compiler build is the step that fails first and blocks everything after - # it, so its output is captured exactly like a case command's. - build = "call BUILD.BAT" if trace_dos else "call BUILD.BAT > RESULTS\\BUILD.LOG" - if prebuilt: - # FEC.EXE was restored from cache; BUILD.BAT would delete and rebuild it. - # It also puts the Watcom binaries on PATH, which the case commands need - # after it, so that line has to be reproduced rather than skipped. - build = ("set PATH=%WATCOM%\\BINW;%WATCOM%\\BINP;%PATH%\r\n" - "echo OK>BUILD.OK") - lines = [ - "@echo off", "if not exist RESULTS md RESULTS", "if not exist OUT md OUT", - "set WATCOM=W:", "set INCLUDE=W:\\H", - "set LIB=W:\\LIB286\\DOS;W:\\LIB286;W:\\LIB386\\DOS;W:\\LIB386", - # COMMAND.COM can only redirect handle 1, so fec diagnostics written to - # stderr never reach RESULTS\.LOG. Ask it for stdout instead. - "set FE_DIAG_STDOUT=1", - build, "if not exist BUILD.OK goto BUILDFAIL", - "echo PASS>RESULTS\\BUILD.RES", - ] - for index, case in enumerate(cases): - key = f"C{index:03d}" - command = case.command - # Watcom writes diagnostics into the current directory. Isolate each - # case so a later pytest item never sees stale diagnostics. - lines.extend([ - "if exist *.ERR del *.ERR > NUL", - f"if exist RESULTS\\{key}.ERR del RESULTS\\{key}.ERR > NUL", - ]) - if not trace_dos: - command += f" > RESULTS\\{key}.LOG" - lines.extend([ - command, - f"call RC.BAT {key}", - f"if exist *.ERR type *.ERR > RESULTS\\{key}.ERR", - f"if not exist RESULTS\\{key}.ERR type NUL > RESULTS\\{key}.ERR", - ]) - lines.extend([ - "goto FINISH", ":BUILDFAIL", "echo FAIL>RESULTS\\BUILD.RES", ":FINISH", - "echo DONE>RUN.OK", - *(["pause"] if show_dos else []), "exit", "", - ]) - return "\r\n".join(lines) - - -@dataclass -class SuiteRun: - root: Path - cases: list[Case] - keep: bool = False - - @property - def fec(self) -> Path: - return self.root / "FEC" - - def _key(self, case: Case) -> str: - return f"C{self.cases.index(case):03d}" - - def rc(self, case: Case) -> int | None: - """Exit code the DOS command reported, or None if it was never recorded. - - Values above 8 are a lower bound; see ``_RC_LADDER``. - """ - path = self.fec / "RESULTS" / f"{self._key(case)}.RC" - if not path.is_file(): - return None - text = path.read_text(encoding="ascii", errors="replace").strip() - return int(text) if text.isdigit() else None - - def result(self, case: Case | None = None) -> str: - if case is None: - path = self.fec / "RESULTS" / "BUILD.RES" - return path.read_text(encoding="ascii").strip() if path.is_file() else "MISSING" - code = self.rc(case) - if code is None: - return "MISSING" - return "PASS" if (code == 0) == case.expect_success else "FAIL" - - def log(self, case: Case | None = None) -> str: - name = "BUILD" if case is None else self._key(case) - path = self.fec / "RESULTS" / f"{name}.LOG" - content = path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" - if content.strip(): - return content - errors = sorted(self.fec.glob("*.ERR")) - joined = "\n".join(p.read_text(encoding="utf-8", errors="replace") for p in errors) - if joined.strip(): - return joined - # Deliberately not falling back to CONSOLE.LOG: that is the emulator's own - # log (display enumeration, INT15 chatter) and burying one useful line in - # it reads as output when there was none. Use --dos-log to see it. - return "(no DOS output captured; the command wrote nothing before exiting)" - - def err(self, case: Case) -> str: - path = self.fec / "RESULTS" / f"{self._key(case)}.ERR" - return path.read_text(encoding="utf-8", errors="replace") if path.is_file() else "" - - def cleanup(self) -> None: - if not self.keep: - shutil.rmtree(self.root, ignore_errors=True) - - -def _compiler_key() -> str: - """Hash of everything the compiler build reads. - - Sources and the build batch only; the toolchain itself is pinned by - dosboxx.lock.json, so it cannot drift underneath a cache hit. - """ - digest = hashlib.sha256() - paths = sorted((ROOT / "fec" / "src").rglob("*")) - paths.append(ROOT / "fec" / "build-dos.bat") - for path in paths: - if not path.is_file(): - continue - digest.update(path.name.encode("utf-8")) - digest.update(path.read_bytes()) - return digest.hexdigest()[:16] - - -def run_suite(cases: list[Case], *, keep: bool = False, show_dos: bool = False, - trace_dos: bool = False) -> SuiteRun: - dosbox, watcom = resolve_tools() - RUNS.mkdir(parents=True, exist_ok=True) - run_root = Path(tempfile.mkdtemp(prefix="suite-", dir=RUNS)) - result = SuiteRun(run_root, cases, keep) - fec = result.fec - cached = CACHE / "compilers" / f"{_compiler_key()}.exe" - try: - shutil.copytree(ROOT / "fec" / "src", fec / "SRC") - shutil.copytree(ROOT / "fec" / "std", fec / "STD") - shutil.copytree(ROOT / "fec" / "tests", fec / "TESTS") - shutil.copy2(ROOT / "fec" / "build-dos.bat", fec / "BUILD.BAT") - console = run_root / "CONSOLE.LOG" - config = run_root / "DOSBOX.CON" - config.write_text( - # core=auto leaves real mode on the interpreter, which is where the - # 16-bit compiler build spends its time. Nothing here is timing - # sensitive -- a compiler and a batch file -- so ask for the - # recompiler explicitly. The M3 bounds cases confirm abort() still - # reports its exit status under it. - f"[log]\nlogfile={console}\n" - f"[dosbox]\nlog console=quiet\n" - f"[cpu]\ncore=dynamic\ncycles=max\n", - encoding="ascii", - ) - if cached.is_file(): - shutil.copy2(cached, fec / "FEC.EXE") - (fec / "RUN.BAT").write_text( - _batch(cases, show_dos=show_dos, trace_dos=trace_dos, - prebuilt=cached.is_file()), - encoding="ascii", newline="", - ) - (fec / "RC.BAT").write_text(_rc_batch(), encoding="ascii", newline="") - command = [str(dosbox)] - if not show_dos: - command.append("-silent") - command.extend([ - "-fastlaunch", "-conf", str(config), - "-c", f'mount C "{run_root}"', "-c", f'mount R "{ROOT}" -ro', - "-c", f'mount W "{watcom}" -ro', - "-c", "C:", "-c", "cd \\FEC", "-c", "RUN.BAT", - ]) - # Every case pays a DOS process spawn, and the compile-only checks spawn - # wcc386 once each, so the whole-suite run is minutes rather than the - # under-a-minute a single milestone takes. - completed = subprocess.run(command, check=False, timeout=1800) - if completed.returncode != 0: - raise DosboxError(f"DOSBox-X exited with status {completed.returncode}") - if not (fec / "RUN.OK").is_file(): - raise DosboxError("DOSBox-X did not complete the test batch") - built = fec / "FEC.EXE" - if not cached.is_file() and built.is_file() and result.result() == "PASS": - cached.parent.mkdir(parents=True, exist_ok=True) - shutil.copy2(built, cached) - return result - except Exception: - result.keep = True - raise diff --git a/src/ferrolang_vm/paths.py b/src/ferrolang_vm/paths.py deleted file mode 100644 index 87ffbbd..0000000 --- a/src/ferrolang_vm/paths.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Repository and local cache paths shared by Ferro developer tools.""" -from pathlib import Path - - -ROOT = Path(__file__).resolve().parents[2] diff --git a/src/ferrolang_vm/registry.py b/src/ferrolang_vm/registry.py deleted file mode 100644 index 5a47497..0000000 --- a/src/ferrolang_vm/registry.py +++ /dev/null @@ -1,260 +0,0 @@ -"""Milestone case registry: DOS commands and their expected exit status. - -Only commands live here; the ``.fe`` fixtures stay under ``fec/tests`` and are -copied into the disposable DOS filesystem by the runner. Case order is load -bearing -- ``emit`` must precede ``build`` must precede ``run`` for the same -fixture, because each step consumes the previous step's output. -""" -from __future__ import annotations - -from .suite import Case - -PASS = "TESTS\\PASS" -FAIL = "TESTS\\FAIL" -STD = "STD" -M2 = "TESTS\\M2" -M3 = "TESTS\\M3" -M4 = "TESTS\\M4" -M5 = "TESTS\\M5" -M6 = "TESTS\\M6" -M7 = "TESTS\\M7" -OUT = "OUT" - -# Emitted-C basenames that were hand-shortened for DOS 8.3. Keyed by milestone -# because the same fixture name maps to different outputs across milestones -# (``bad-type`` is BAD-TY in M2 but BAD-TYP in M4). The shortenings are not -# consistent -- M2 cut to six characters, M4 to seven, and several were never -# required at all since BAD-COND is already a legal 8.3 name. Preserved verbatim; -# changing one renames a file inside the DOS run, so re-verify if you touch it. -_OUT83 = { - (2, "bad-cond"): "BAD-CO", - (2, "bad-cast"): "BAD-CA", - (2, "bad-asgn"): "BAD-AS", - (2, "bad-unk"): "BAD-UN", - (2, "bad-ari"): "BAD-AR", - (2, "bad-type"): "BAD-TY", - (2, "bad-ret"): "BAD-RE", - (2, "bad-unit"): "BAD-UI", - (2, "bad-void"): "BAD-VO", - (4, "bad-type"): "BAD-TYP", - (4, "bad-writ"): "BAD-WRI", - (5, "bad-dest"): "BAD-DES", -} - - -def _case(milestone: int, name: str, command: str, ok: bool = True) -> Case: - return Case(f"m{milestone}-{name}", milestone, command, ok) - - -def _fe(directory: str, name: str) -> str: - return f"{directory}\\{name.upper()}.FE" - - -def _emit(source: str, output: str, *, target: str = "bits32", - flags: tuple[str, ...] = (), output_first: bool = False) -> str: - """``fec`` invocation that translates ``source`` to C at ``output``. - - ``output_first`` reproduces the M6 cases, which pass ``-o`` before the input - file while every other milestone passes it after. - """ - parts = ["FEC.EXE", f"--target={target}", *flags, "--emit-c"] - parts += ["-o", output, source] if output_first else [source, "-o", output] - return " ".join(parts) - - -def _wcl(exe: str, *sources: str, bits: int = 32, strict: bool = False, - defines: tuple[str, ...] = ()) -> str: - """Open Watcom invocation. ``strict`` is the M4 ``-wx -wcd=202`` pairing: - warnings are errors except W202, which the generated C trips on unused - helpers (see AGENTS.md).""" - parts = ["WCL386" if bits == 32 else "WCL", "-q", "-za"] - if strict: - parts += ["-wx", "-wcd=202"] - parts += ["-bt=dos", *defines, f"-fe={exe}", *sources] - return " ".join(parts) - - -def _wcc(source: str, obj: str) -> str: - """Compile the generated C without linking. - - A fixture with no ``main`` cannot be run, so this is the floor for it: the - backend's output has to survive the compiler the project actually ships - with. It catches a malformed emission -- an unnamed assignment target, a - helper that is called but never defined, an initializer C89 rejects -- which - otherwise sits unnoticed in a case that only ever emitted text. - - This asserts nothing about the C itself; it is a conformance check on the - backend's output, so a future non-C backend swaps the command rather than - the intent. Never grep the generated C to prove a language feature -- write a - fixture whose exit code differs instead, as TESTS\\M3\\NOCHK.FE does. - """ - # Through the wcl386 driver with -c rather than calling wcc386 directly: - # wcc386 writes its diagnostics to stderr, which COMMAND.COM cannot - # redirect, so a failure would report a count and no messages. The driver - # leaves an .ERR file, which the runner already collects. - return f"WCL386 -q -za -wx -wcd=202 -bt=dos -c -fo={obj} {source}" - - -def _accepts(milestone: int, directory: str, names: tuple[str, ...], *, - output: str = OUT, output_first: bool = True) -> list[Case]: - """Fixtures that must compile: emit the C, then build it.""" - cases: list[Case] = [] - for name in names: - cfile = f"{output}\\{name.upper()}.C" - cases.append(_case(milestone, name, - _emit(_fe(directory, name), cfile, - output_first=output_first))) - cases.append(_case(milestone, f"{name}-cc", - _wcc(cfile, f"{OUT}\\{name.upper()}.OBJ"))) - return cases - - -def _dump_ast(milestone: int, directory: str, names: tuple[str, ...], *, - suffix: str, ok: bool = True, prefix: str = "") -> list[Case]: - return [ - _case(milestone, f"{prefix}{name}-{suffix}", - f"FEC.EXE --dump-ast {_fe(directory, name)}", ok) - for name in names - ] - - -def _rejects(milestone: int, directory: str, names: tuple[str, ...], *, - suffix: str = "") -> list[Case]: - """Fixtures that must fail to compile. The emitted-C path is still spelled - out because ``fec`` needs an ``-o`` even when it is expected to bail.""" - return [ - _case(milestone, f"{name}-{suffix}" if suffix else name, - _emit(_fe(directory, name), - f"{directory}\\{_OUT83.get((milestone, name), name.upper())}.C"), - False) - for name in names - ] - - -def _triple(milestone: int, name: str, directory: str, *, stem: str | None = None, - target: str = "bits32", bits: int = 32, strict: bool = False, - build_source: str | None = None, emit_suffix: str | None = "emit", - run_suffix: str = "run", run_ok: bool = True) -> list[Case]: - """emit -> build -> run for one fixture. - - ``stem`` renames the C/EXE pair when the fixture name does not fit 8.3 or - collides (M2 castwhil emits CAST16). ``build_source`` compiles a different - file than the one emitted (M4 prop emits PROP.C but builds PROPTEST.C, which - ``#include``s it). - """ - stem = stem or name.upper() - cfile = f"{directory}\\{stem}.C" - exe = f"{directory}\\{stem}.EXE" - emit_id = f"{name}-{emit_suffix}" if emit_suffix else name - return [ - _case(milestone, emit_id, _emit(_fe(directory, name), cfile, target=target)), - _case(milestone, f"{name}-build", - _wcl(exe, build_source or cfile, bits=bits, strict=strict)), - _case(milestone, f"{name}-{run_suffix}", exe, run_ok), - ] - - -CASES: list[Case] = [ - # -- M1: parse only ------------------------------------------------------- - *_dump_ast(1, PASS, ("basic", "literals", "keybuilt", "v012form"), suffix="parse"), - *_dump_ast(1, STD, ("core", "fmt", "io", "list", "map", "mem", "str", "sys"), - suffix="parse", prefix="std-"), - *_dump_ast(1, FAIL, ("misssemi", "unclcomm", "logical"), suffix="reject", ok=False), - - # -- M2: first generated C ------------------------------------------------ - *_triple(2, "hello", M2), - *_triple(2, "scopes", M2), - *_triple(2, "castwhil", M2, stem="CAST16", target="bits16", bits=16), - *_rejects(2, M2, ("bad-cond", "bad-cast", "bad-asgn", "bad-unk", "bad-ari", - "bad-type", "bad-ret", "bad-unit", "bad-void"), suffix="reject"), - - # -- M3: aggregates, strings, bounds checks ------------------------------- - *_triple(3, "struct", M3), - *_triple(3, "enum", M3), - *_triple(3, "array", M3), - *_triple(3, "mutable", M3), - *_rejects(3, M3, ("bad-mlet", "bad-shwr"), suffix="reject"), - *_triple(3, "str", M3), - *_triple(3, "for", M3), - *_triple(3, "nested", M3), - *_triple(3, "char", M3), - *_triple(3, "arrayctx", M3), - # These two must trap at runtime: the bounds check is the feature under test. - *_triple(3, "bounds", M3, run_suffix="trap", run_ok=False), - *_triple(3, "slcbound", M3, run_suffix="trap", run_ok=False), - # --no-checks is proved by a differential on one source. NOCHK.FE reads one - # element past a [2]i32 and returns x - x, which is 0 whatever garbage the - # unchecked read produced: compiled with checks it must trap, compiled with - # --no-checks it must run to completion. BOUNDS.FE cannot serve as the - # unchecked half because it returns the out-of-bounds value directly, so its - # exit code would be whatever happens to sit past the array on the stack. - *_triple(3, "nochk", M3, run_suffix="trap", run_ok=False), - _case(3, "nochk-off-emit", - _emit(_fe(M3, "nochk"), f"{M3}\\NOCHK-N.C", flags=("--no-checks",))), - _case(3, "nochk-off-build", - _wcl(f"{M3}\\NOCHK-N.EXE", f"{M3}\\NOCHK-N.C")), - _case(3, "nochk-off-run", f"{M3}\\NOCHK-N.EXE"), - *_rejects(3, M3, ("badfld", "badmat", "badarr", "badcycle", "badstr", "badchar", - "badfield", "badindex"), suffix="reject"), - - # -- M4: formatting and error propagation --------------------------------- - *_triple(4, "format", M4, strict=True, emit_suffix=None), - *_triple(4, "try-fpr", M4, strict=True, emit_suffix=None), - *_triple(4, "prop", M4, strict=True, emit_suffix=None, - build_source=f"{M4}\\PROPTEST.C"), - *_rejects(4, M4, ("bad-ari", "bad-verb", "bad-run", "bad-type", "bad-try", - "bad-writ", "bad-bufw", "bad-many", "bad-open", "bad-cls")), - - # -- M5: defer and ownership ---------------------------------------------- - *_accepts(5, M5, ("defer", "owned"), output=M5, output_first=False), - *_rejects(5, M5, ("bad-move", "bad-dest", "bad-drop", "bad-dbl", "bad-cond", - "bad-proj", "bad-clos", "bad-loop")), - # The runtime case links the generated C against a hand-written allocator - # shim, so malloc/free are redirected at compile time. - _case(5, "runtime", _emit(_fe(M5, "runtime"), f"{M5}\\RUNT-G.C")), - _case(5, "runtime-build", - _wcl(f"{M5}\\RUNTIME.EXE", f"{M5}\\RUNT-G.C", f"{M5}\\RUNTIME.C", - defines=("-dmalloc=m5_malloc", "-dfree=m5_free"))), - _case(5, "runtime-run", f"{M5}\\RUNTIME.EXE"), - - # -- M6: borrow checking (R1--R8) ----------------------------------------- - *[_case(6, name, f"FEC.EXE --check {_fe(M6, name)}", False) for name in ( - "badarg", "badbinit", "badbrmov", "baddefer", "badfld", "badglob", "badgmut", - "badinv", "badlocsl", "badloop", "badmove", "badmut", "badmut2", "badptr", - "badret", "badrfld", "badridx", "badscop", "badself", "badshwr", "badslfld", - "badtwo", "badup", "badweak")], - *_accepts(6, M6, ( - "okbranch", "okdefer", "okglobcp", "oklast", "okr8free", "okr8join", - "okr8meth", "okr8stat", "okrebor", "okrtlast", "okshare", "okslreb", - "okstatic", "oktemp", "oktrim", "okwcall")), - - # -- M7: optionals and error unions --------------------------------------- - *[_case(7, name, f"FEC.EXE --check {_fe(M7, name)}", False) for name in ( - "badcatch", "baddef", "baddir", "badercod", "badernam", "badetype", - "badnull", "badoref", "badorel", "badproj", "badqmark", "badret", - "badsome", "badtry", "badzero")], - *_accepts(7, M7, ( - "okcatch", "okcatmov", "okcvoid", "okdeflt", "okiflet", "okmatch", - "oknull", "okorelse", "okpatvw", "okproj", "okrepl", "oktrdef", - "oktry")), -] - -MAX_MILESTONE: int = max(case.milestone for case in CASES) -MILESTONES: tuple[str, ...] = tuple(f"m{number}" - for number in range(1, MAX_MILESTONE + 1)) - - -def milestone_number(name: str) -> int: - """Parse an ``mN`` selector against the milestones the registry knows about.""" - if not name.startswith("m") or not name[1:].isdigit(): - raise ValueError(f"invalid milestone: {name}") - value = int(name[1:]) - if value not in range(1, MAX_MILESTONE + 1): - raise ValueError(f"unsupported milestone: {name}") - return value - - -def all_cases(*, through: int = MAX_MILESTONE, only: int | None = None) -> list[Case]: - if only is not None: - return [case for case in CASES if case.milestone == only] - return [case for case in CASES if case.milestone <= through] diff --git a/src/ferrolang_vm/suite.py b/src/ferrolang_vm/suite.py deleted file mode 100644 index b412298..0000000 --- a/src/ferrolang_vm/suite.py +++ /dev/null @@ -1,16 +0,0 @@ -"""The one type shared by the case registry and the DOSBox-X runner. - -Kept separate from ``registry`` so that ``dosboxx`` can depend on the type -without importing the case data. -""" -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass(frozen=True) -class Case: - id: str - milestone: int - command: str - expect_success: bool diff --git a/src/ferrolang_vm/test_cli.py b/src/ferrolang_vm/test_cli.py deleted file mode 100644 index d82b2ee..0000000 --- a/src/ferrolang_vm/test_cli.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Developer test entry point backed by a disposable DOSBox-X run.""" -from __future__ import annotations - -import argparse -import os -import sys -from pathlib import Path - -from .dosboxx import DosboxError, setup -from .registry import MAX_MILESTONE, MILESTONES - - -def main() -> int: - parser = argparse.ArgumentParser( - prog="ferro-test", - description="Fast Ferro development tests in DOSBox-X/Open Watcom.", - ) - commands = parser.add_subparsers(dest="command", required=True) - prepare = commands.add_parser("setup", help="download and verify pinned development tools") - prepare.add_argument("--accept-watcom-license", action="store_true", - help="confirm acceptance of the Sybase Open Watcom Public License") - run = commands.add_parser("run", help="build once and run milestone pytest cases") - selection = run.add_mutually_exclusive_group() - selection.add_argument("--through", choices=MILESTONES, default=f"m{MAX_MILESTONE}", - help="run cumulatively through this milestone " - f"(default: m{MAX_MILESTONE})") - selection.add_argument("--only", choices=MILESTONES, - help="run only this milestone's cases") - run.add_argument("-v", "--verbose", action="store_true", help="show every pytest case") - run.add_argument("--keep-failed", action="store_true", - help="keep the disposable DOS filesystem after failures") - run.add_argument("--show-dos", action="store_true", - help="show DOSBox-X and wait for a key before closing") - run.add_argument("--dos-log", action="store_true", - help="print the captured DOS console after the run") - run.add_argument("--trace-dos", action="store_true", - help="do not redirect case command output") - run.add_argument("-k", dest="select", metavar="EXPR", - help="run only cases whose id matches this pytest -k expression") - args, extra = parser.parse_known_args() - try: - if args.command == "setup": - dosbox, watcom = setup(accept_watcom_license=args.accept_watcom_license) - print(f"DOSBox-X: {dosbox}") - print(f"Open Watcom: {watcom}") - return 0 - if args.only: - os.environ["FERRO_TEST_ONLY"] = args.only - else: - os.environ["FERRO_TEST_THROUGH"] = args.through - for enabled, name in ( - (args.keep_failed, "FERRO_TEST_KEEP_FAILED"), - (args.show_dos, "FERRO_TEST_SHOW_DOS"), - (args.trace_dos, "FERRO_TEST_TRACE_DOS"), - (args.dos_log, "FERRO_TEST_DOS_LOG"), - ): - if enabled: - os.environ[name] = "1" - import pytest - from .paths import ROOT - # The package can be imported from a different checkout than the one the - # shell is sitting in -- an editable install plus a git worktree is enough - # to silently build and test the wrong tree. Say which tree this is. - print(f"ferro-test: building {ROOT}", file=sys.stderr) - tests = ROOT / "tools" / "tests" - # The host gates run first and take under a second. A missing declaration - # or an 8.3-illegal name would otherwise be found only after a DOSBox-X - # boot and a full compiler build, and the DOS-side message for either is - # unhelpful. They do not replace the DOS run; they precede it. - gates = [os.fspath(tests / name) for name in - ("test_host_syntax.py", "test_dos_names.py")] - if pytest.main([*gates, "-q", "--no-header"]) != 0: - print("ferro-test: host gates failed; not starting DOSBox-X", - file=sys.stderr) - return 1 - test_file = os.fspath(tests / "test_milestones_dosboxx.py") - pytest_args = [test_file, "--tb=short", "-v" if args.verbose else "-q"] - if args.select: - pytest_args.extend(["-k", args.select]) - if args.dos_log: - pytest_args.append("-s") - pytest_args.extend(extra) - return int(pytest.main(pytest_args)) - except (DosboxError, ValueError) as exc: - print(f"ferro-test: {exc}", file=sys.stderr) - return 2 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/src/node-types.json b/src/node-types.json deleted file mode 100644 index ae9616e..0000000 --- a/src/node-types.json +++ /dev/null @@ -1,121 +0,0 @@ -[ - { - "type": "block_comment", - "named": true, - "fields": {}, - "children": { - "multiple": true, - "required": false, - "types": [ - { - "type": "block_comment", - "named": true - } - ] - } - }, - { - "type": "source_file", - "named": true, - "root": true, - "fields": {}, - "children": { - "multiple": true, - "required": false, - "types": [ - { - "type": "block_comment", - "named": true - }, - { - "type": "builtin", - "named": true - }, - { - "type": "builtin_type", - "named": true - }, - { - "type": "char_literal", - "named": true - }, - { - "type": "identifier", - "named": true - }, - { - "type": "integer_literal", - "named": true - }, - { - "type": "keyword", - "named": true - }, - { - "type": "line_comment", - "named": true - }, - { - "type": "operator", - "named": true - }, - { - "type": "punctuation", - "named": true - }, - { - "type": "string_literal", - "named": true - } - ] - } - }, - { - "type": "*/", - "named": false - }, - { - "type": "/*", - "named": false - }, - { - "type": "builtin", - "named": true - }, - { - "type": "builtin_type", - "named": true - }, - { - "type": "char_literal", - "named": true - }, - { - "type": "identifier", - "named": true - }, - { - "type": "integer_literal", - "named": true - }, - { - "type": "keyword", - "named": true - }, - { - "type": "line_comment", - "named": true - }, - { - "type": "operator", - "named": true - }, - { - "type": "punctuation", - "named": true - }, - { - "type": "string_literal", - "named": true - } -] \ No newline at end of file diff --git a/src/parser.c b/src/parser.c deleted file mode 100644 index 9173d00..0000000 --- a/src/parser.c +++ /dev/null @@ -1,1744 +0,0 @@ -/* Automatically @generated by tree-sitter */ - -#include "tree_sitter/parser.h" - -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic ignored "-Wmissing-field-initializers" -#endif - -#define LANGUAGE_VERSION 14 -#define STATE_COUNT 14 -#define LARGE_STATE_COUNT 6 -#define SYMBOL_COUNT 20 -#define ALIAS_COUNT 0 -#define TOKEN_COUNT 16 -#define EXTERNAL_TOKEN_COUNT 0 -#define FIELD_COUNT 0 -#define MAX_ALIAS_SEQUENCE_LENGTH 3 -#define MAX_RESERVED_WORD_SET_SIZE 0 -#define PRODUCTION_ID_COUNT 1 -#define SUPERTYPE_COUNT 0 - -enum ts_symbol_identifiers { - sym_line_comment = 1, - anon_sym_SLASH_STAR = 2, - aux_sym_block_comment_token1 = 3, - aux_sym_block_comment_token2 = 4, - aux_sym_block_comment_token3 = 5, - anon_sym_STAR_SLASH = 6, - sym_string_literal = 7, - sym_char_literal = 8, - sym_integer_literal = 9, - sym_builtin = 10, - sym_builtin_type = 11, - sym_keyword = 12, - sym_identifier = 13, - sym_operator = 14, - sym_punctuation = 15, - sym_source_file = 16, - sym_block_comment = 17, - aux_sym_source_file_repeat1 = 18, - aux_sym_block_comment_repeat1 = 19, -}; - -static const char * const ts_symbol_names[] = { - [ts_builtin_sym_end] = "end", - [sym_line_comment] = "line_comment", - [anon_sym_SLASH_STAR] = "/*", - [aux_sym_block_comment_token1] = "block_comment_token1", - [aux_sym_block_comment_token2] = "block_comment_token2", - [aux_sym_block_comment_token3] = "block_comment_token3", - [anon_sym_STAR_SLASH] = "*/", - [sym_string_literal] = "string_literal", - [sym_char_literal] = "char_literal", - [sym_integer_literal] = "integer_literal", - [sym_builtin] = "builtin", - [sym_builtin_type] = "builtin_type", - [sym_keyword] = "keyword", - [sym_identifier] = "identifier", - [sym_operator] = "operator", - [sym_punctuation] = "punctuation", - [sym_source_file] = "source_file", - [sym_block_comment] = "block_comment", - [aux_sym_source_file_repeat1] = "source_file_repeat1", - [aux_sym_block_comment_repeat1] = "block_comment_repeat1", -}; - -static const TSSymbol ts_symbol_map[] = { - [ts_builtin_sym_end] = ts_builtin_sym_end, - [sym_line_comment] = sym_line_comment, - [anon_sym_SLASH_STAR] = anon_sym_SLASH_STAR, - [aux_sym_block_comment_token1] = aux_sym_block_comment_token1, - [aux_sym_block_comment_token2] = aux_sym_block_comment_token2, - [aux_sym_block_comment_token3] = aux_sym_block_comment_token3, - [anon_sym_STAR_SLASH] = anon_sym_STAR_SLASH, - [sym_string_literal] = sym_string_literal, - [sym_char_literal] = sym_char_literal, - [sym_integer_literal] = sym_integer_literal, - [sym_builtin] = sym_builtin, - [sym_builtin_type] = sym_builtin_type, - [sym_keyword] = sym_keyword, - [sym_identifier] = sym_identifier, - [sym_operator] = sym_operator, - [sym_punctuation] = sym_punctuation, - [sym_source_file] = sym_source_file, - [sym_block_comment] = sym_block_comment, - [aux_sym_source_file_repeat1] = aux_sym_source_file_repeat1, - [aux_sym_block_comment_repeat1] = aux_sym_block_comment_repeat1, -}; - -static const TSSymbolMetadata ts_symbol_metadata[] = { - [ts_builtin_sym_end] = { - .visible = false, - .named = true, - }, - [sym_line_comment] = { - .visible = true, - .named = true, - }, - [anon_sym_SLASH_STAR] = { - .visible = true, - .named = false, - }, - [aux_sym_block_comment_token1] = { - .visible = false, - .named = false, - }, - [aux_sym_block_comment_token2] = { - .visible = false, - .named = false, - }, - [aux_sym_block_comment_token3] = { - .visible = false, - .named = false, - }, - [anon_sym_STAR_SLASH] = { - .visible = true, - .named = false, - }, - [sym_string_literal] = { - .visible = true, - .named = true, - }, - [sym_char_literal] = { - .visible = true, - .named = true, - }, - [sym_integer_literal] = { - .visible = true, - .named = true, - }, - [sym_builtin] = { - .visible = true, - .named = true, - }, - [sym_builtin_type] = { - .visible = true, - .named = true, - }, - [sym_keyword] = { - .visible = true, - .named = true, - }, - [sym_identifier] = { - .visible = true, - .named = true, - }, - [sym_operator] = { - .visible = true, - .named = true, - }, - [sym_punctuation] = { - .visible = true, - .named = true, - }, - [sym_source_file] = { - .visible = true, - .named = true, - }, - [sym_block_comment] = { - .visible = true, - .named = true, - }, - [aux_sym_source_file_repeat1] = { - .visible = false, - .named = false, - }, - [aux_sym_block_comment_repeat1] = { - .visible = false, - .named = false, - }, -}; - -static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = { - [0] = {0}, -}; - -static const uint16_t ts_non_terminal_alias_map[] = { - 0, -}; - -static const TSStateId ts_primary_state_ids[STATE_COUNT] = { - [0] = 0, - [1] = 1, - [2] = 2, - [3] = 3, - [4] = 4, - [5] = 5, - [6] = 6, - [7] = 7, - [8] = 8, - [9] = 6, - [10] = 7, - [11] = 4, - [12] = 5, - [13] = 13, -}; - -static bool ts_lex(TSLexer *lexer, TSStateId state) { - START_LEXER(); - eof = lexer->eof(lexer); - switch (state) { - case 0: - if (eof) ADVANCE(21); - ADVANCE_MAP( - '!', 155, - '"', 1, - '%', 155, - '&', 155, - '\'', 6, - '*', 157, - '+', 157, - '-', 159, - '.', 161, - '/', 153, - '0', 31, - '<', 154, - '=', 158, - '>', 156, - '@', 20, - 'S', 69, - '^', 155, - 'a', 110, - 'b', 115, - 'c', 48, - 'd', 71, - 'e', 98, - 'f', 50, - 'i', 44, - 'l', 76, - 'm', 49, - 'n', 118, - 'o', 126, - 'p', 51, - 'r', 78, - 's', 68, - 't', 127, - 'u', 45, - 'v', 53, - 'w', 86, - '|', 155, - '?', 152, - '~', 152, - '(', 160, - ')', 160, - ',', 160, - ':', 160, - ';', 160, - '[', 160, - ']', 160, - '{', 160, - '}', 160, - ); - if (('\t' <= lookahead && lookahead <= '\r') || - lookahead == ' ' || - lookahead == 0x200b || - lookahead == 0x2060 || - lookahead == 0xfeff) SKIP(0); - if (('1' <= lookahead && lookahead <= '9')) ADVANCE(34); - if (('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('g' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 1: - if (lookahead == '"') ADVANCE(29); - if (lookahead == '\\') ADVANCE(7); - if (lookahead != 0 && - lookahead != '\n' && - lookahead != '\r' && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(1); - END_STATE(); - case 2: - if (lookahead == '\'') ADVANCE(30); - END_STATE(); - case 3: - if (lookahead == '*') ADVANCE(23); - if (lookahead != 0 && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(27); - END_STATE(); - case 4: - if (lookahead == '*') ADVANCE(5); - if (lookahead == '/') ADVANCE(3); - if (('\t' <= lookahead && lookahead <= '\r') || - lookahead == ' ' || - lookahead == 0x200b || - lookahead == 0x2060 || - lookahead == 0xfeff) ADVANCE(24); - if (lookahead != 0 && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(25); - END_STATE(); - case 5: - if (lookahead == '/') ADVANCE(28); - if (lookahead != 0 && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(26); - END_STATE(); - case 6: - if (lookahead == '\\') ADVANCE(8); - if (lookahead != 0 && - lookahead != '\n' && - lookahead != '\r' && - lookahead != '\'' && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(2); - END_STATE(); - case 7: - ADVANCE_MAP( - 'u', 17, - 'x', 13, - '"', 1, - '\'', 1, - '0', 1, - '\\', 1, - 'n', 1, - 'r', 1, - 't', 1, - ); - END_STATE(); - case 8: - ADVANCE_MAP( - 'u', 18, - 'x', 14, - '"', 2, - '\'', 2, - '0', 2, - '\\', 2, - 'n', 2, - 'r', 2, - 't', 2, - ); - END_STATE(); - case 9: - if (lookahead == '0' || - lookahead == '1' || - lookahead == '_') ADVANCE(32); - END_STATE(); - case 10: - if (('0' <= lookahead && lookahead <= '7') || - lookahead == '_') ADVANCE(33); - END_STATE(); - case 11: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(1); - END_STATE(); - case 12: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(2); - END_STATE(); - case 13: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(11); - END_STATE(); - case 14: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(12); - END_STATE(); - case 15: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(13); - END_STATE(); - case 16: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(14); - END_STATE(); - case 17: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(15); - END_STATE(); - case 18: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(16); - END_STATE(); - case 19: - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(35); - END_STATE(); - case 20: - if (('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(36); - END_STATE(); - case 21: - ACCEPT_TOKEN(ts_builtin_sym_end); - END_STATE(); - case 22: - ACCEPT_TOKEN(sym_line_comment); - if (lookahead != 0 && - lookahead != '\n' && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(22); - END_STATE(); - case 23: - ACCEPT_TOKEN(anon_sym_SLASH_STAR); - END_STATE(); - case 24: - ACCEPT_TOKEN(aux_sym_block_comment_token1); - if (('\t' <= lookahead && lookahead <= '\r') || - lookahead == ' ' || - lookahead == 0x200b || - lookahead == 0x2060 || - lookahead == 0xfeff) ADVANCE(24); - if (lookahead != 0 && - lookahead != '*' && - lookahead != '/' && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(25); - END_STATE(); - case 25: - ACCEPT_TOKEN(aux_sym_block_comment_token1); - if (lookahead != 0 && - lookahead != '*' && - lookahead != '/' && - lookahead != 0x17f && - lookahead != 0x212a) ADVANCE(25); - END_STATE(); - case 26: - ACCEPT_TOKEN(aux_sym_block_comment_token2); - END_STATE(); - case 27: - ACCEPT_TOKEN(aux_sym_block_comment_token3); - END_STATE(); - case 28: - ACCEPT_TOKEN(anon_sym_STAR_SLASH); - END_STATE(); - case 29: - ACCEPT_TOKEN(sym_string_literal); - END_STATE(); - case 30: - ACCEPT_TOKEN(sym_char_literal); - END_STATE(); - case 31: - ACCEPT_TOKEN(sym_integer_literal); - if (lookahead == 'B' || - lookahead == 'b') ADVANCE(9); - if (lookahead == 'O' || - lookahead == 'o') ADVANCE(10); - if (lookahead == 'X' || - lookahead == 'x') ADVANCE(19); - if (('0' <= lookahead && lookahead <= '9') || - lookahead == '_') ADVANCE(34); - END_STATE(); - case 32: - ACCEPT_TOKEN(sym_integer_literal); - if (lookahead == '0' || - lookahead == '1' || - lookahead == '_') ADVANCE(32); - END_STATE(); - case 33: - ACCEPT_TOKEN(sym_integer_literal); - if (('0' <= lookahead && lookahead <= '7') || - lookahead == '_') ADVANCE(33); - END_STATE(); - case 34: - ACCEPT_TOKEN(sym_integer_literal); - if (('0' <= lookahead && lookahead <= '9') || - lookahead == '_') ADVANCE(34); - END_STATE(); - case 35: - ACCEPT_TOKEN(sym_integer_literal); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'F') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'f')) ADVANCE(35); - END_STATE(); - case 36: - ACCEPT_TOKEN(sym_builtin); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(36); - END_STATE(); - case 37: - ACCEPT_TOKEN(sym_builtin_type); - END_STATE(); - case 38: - ACCEPT_TOKEN(sym_builtin_type); - if (lookahead == 'u') ADVANCE(63); - END_STATE(); - case 39: - ACCEPT_TOKEN(sym_keyword); - END_STATE(); - case 40: - ACCEPT_TOKEN(sym_keyword); - if (lookahead == '_') ADVANCE(135); - END_STATE(); - case 41: - ACCEPT_TOKEN(sym_keyword); - if (lookahead == 'e') ADVANCE(100); - END_STATE(); - case 42: - ACCEPT_TOKEN(sym_keyword); - if (lookahead == 'm') ADVANCE(39); - END_STATE(); - case 43: - ACCEPT_TOKEN(sym_keyword); - if (lookahead == 't') ADVANCE(80); - END_STATE(); - case 44: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == '1') ADVANCE(47); - if (lookahead == '3') ADVANCE(46); - if (lookahead == '8') ADVANCE(37); - if (lookahead == 'f') ADVANCE(39); - if (lookahead == 'm') ADVANCE(120); - if (lookahead == 'n') ADVANCE(43); - if (lookahead == 's') ADVANCE(87); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 45: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == '1') ADVANCE(47); - if (lookahead == '3') ADVANCE(46); - if (lookahead == '8') ADVANCE(37); - if (lookahead == 'n') ADVANCE(67); - if (lookahead == 's') ADVANCE(87); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 46: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == '2') ADVANCE(37); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 47: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == '6') ADVANCE(37); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 48: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(140); - if (lookahead == 'h') ADVANCE(54); - if (lookahead == 'o') ADVANCE(107); - if (lookahead == 'r') ADVANCE(93); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 49: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(140); - if (lookahead == 'u') ADVANCE(137); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 50: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(99); - if (lookahead == 'n') ADVANCE(39); - if (lookahead == 'o') ADVANCE(124); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 51: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(61); - if (lookahead == 'u') ADVANCE(59); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 52: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(96); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 53: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(124); - if (lookahead == 'o') ADVANCE(89); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 54: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(125); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 55: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(101); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 56: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(133); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 57: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(83); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 58: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'a') ADVANCE(141); - if (lookahead == 'r') ADVANCE(38); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('b' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 59: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'b') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 60: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'c') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 61: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'c') ADVANCE(97); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 62: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'c') ADVANCE(85); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 63: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'c') ADVANCE(137); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 64: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'c') ADVANCE(55); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 65: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'd') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 66: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'd') ADVANCE(37); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 67: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'd') ADVANCE(77); - if (lookahead == 'i') ADVANCE(137); - if (lookahead == 's') ADVANCE(57); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 68: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(103); - if (lookahead == 'h') ADVANCE(56); - if (lookahead == 't') ADVANCE(58); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 69: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(103); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 70: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(65); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 71: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(82); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 72: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 73: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(37); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 74: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(52); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 75: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(124); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 76: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(137); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 77: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(84); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 78: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(139); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 79: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(128); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 80: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'e') ADVANCE(132); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 81: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'f') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 82: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'f') ADVANCE(75); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 83: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'f') ADVANCE(72); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 84: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'f') ADVANCE(91); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 85: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'h') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 86: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'h') ADVANCE(95); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 87: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(150); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 88: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(112); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 89: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(66); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 90: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(109); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 91: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(113); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 92: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(60); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 93: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(143); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 94: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(64); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 95: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'i') ADVANCE(105); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 96: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'k') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 97: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'k') ADVANCE(70); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 98: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(136); - if (lookahead == 'n') ADVANCE(145); - if (lookahead == 'r') ADVANCE(129); - if (lookahead == 'x') ADVANCE(142); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 99: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(136); - if (lookahead == 'r') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 100: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(136); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 101: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 102: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(37); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 103: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(81); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 104: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(101); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 105: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'l') ADVANCE(72); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 106: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'm') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 107: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'm') ADVANCE(123); - if (lookahead == 'n') ADVANCE(134); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 108: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'm') ADVANCE(92); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 109: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'm') ADVANCE(72); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 110: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'n') ADVANCE(65); - if (lookahead == 's') ADVANCE(42); - if (lookahead == 't') ADVANCE(114); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 111: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'n') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 112: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'n') ADVANCE(147); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 113: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'n') ADVANCE(70); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 114: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'o') ADVANCE(108); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 115: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'o') ADVANCE(117); - if (lookahead == 'r') ADVANCE(74); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 116: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'o') ADVANCE(124); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 117: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'o') ADVANCE(102); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 118: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'o') ADVANCE(137); - if (lookahead == 'u') ADVANCE(104); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 119: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'o') ADVANCE(130); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 120: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'p') ADVANCE(119); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 121: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'p') ADVANCE(72); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 122: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'p') ADVANCE(138); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 123: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'p') ADVANCE(144); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 124: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 125: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(37); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 126: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(41); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 127: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(146); - if (lookahead == 'y') ADVANCE(121); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 128: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(111); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 129: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(116); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 130: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(137); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 131: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(149); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 132: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(131); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 133: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'r') ADVANCE(70); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 134: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 's') ADVANCE(137); - if (lookahead == 't') ADVANCE(88); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 135: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 's') ADVANCE(57); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 136: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 's') ADVANCE(72); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 137: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 138: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(40); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 139: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(148); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 140: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(62); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 141: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(92); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 142: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(79); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 143: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(94); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 144: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 't') ADVANCE(90); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 145: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'u') ADVANCE(106); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 146: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'u') ADVANCE(72); - if (lookahead == 'y') ADVANCE(39); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 147: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'u') ADVANCE(72); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 148: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'u') ADVANCE(128); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 149: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'u') ADVANCE(122); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 150: - ACCEPT_TOKEN(sym_identifier); - if (lookahead == 'z') ADVANCE(73); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'y')) ADVANCE(151); - END_STATE(); - case 151: - ACCEPT_TOKEN(sym_identifier); - if (('0' <= lookahead && lookahead <= '9') || - ('A' <= lookahead && lookahead <= 'Z') || - lookahead == '_' || - ('a' <= lookahead && lookahead <= 'z')) ADVANCE(151); - END_STATE(); - case 152: - ACCEPT_TOKEN(sym_operator); - END_STATE(); - case 153: - ACCEPT_TOKEN(sym_operator); - if (lookahead == '*') ADVANCE(23); - if (lookahead == '/') ADVANCE(22); - if (lookahead == '=') ADVANCE(152); - END_STATE(); - case 154: - ACCEPT_TOKEN(sym_operator); - if (lookahead == '<') ADVANCE(155); - if (lookahead == '=') ADVANCE(152); - END_STATE(); - case 155: - ACCEPT_TOKEN(sym_operator); - if (lookahead == '=') ADVANCE(152); - END_STATE(); - case 156: - ACCEPT_TOKEN(sym_operator); - if (lookahead == '=') ADVANCE(152); - if (lookahead == '>') ADVANCE(155); - END_STATE(); - case 157: - ACCEPT_TOKEN(sym_operator); - if (lookahead == '%' || - lookahead == '=') ADVANCE(152); - END_STATE(); - case 158: - ACCEPT_TOKEN(sym_operator); - if (lookahead == '=' || - lookahead == '>') ADVANCE(152); - END_STATE(); - case 159: - ACCEPT_TOKEN(sym_operator); - if (lookahead == '%' || - lookahead == '=' || - lookahead == '>') ADVANCE(152); - END_STATE(); - case 160: - ACCEPT_TOKEN(sym_punctuation); - END_STATE(); - case 161: - ACCEPT_TOKEN(sym_punctuation); - if (lookahead == '.') ADVANCE(152); - END_STATE(); - default: - return false; - } -} - -static const TSLexMode ts_lex_modes[STATE_COUNT] = { - [0] = {.lex_state = 0}, - [1] = {.lex_state = 0}, - [2] = {.lex_state = 0}, - [3] = {.lex_state = 0}, - [4] = {.lex_state = 0}, - [5] = {.lex_state = 0}, - [6] = {.lex_state = 4}, - [7] = {.lex_state = 4}, - [8] = {.lex_state = 4}, - [9] = {.lex_state = 4}, - [10] = {.lex_state = 4}, - [11] = {.lex_state = 4}, - [12] = {.lex_state = 4}, - [13] = {.lex_state = 0}, -}; - -static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = { - [STATE(0)] = { - [ts_builtin_sym_end] = ACTIONS(1), - [sym_line_comment] = ACTIONS(1), - [anon_sym_SLASH_STAR] = ACTIONS(1), - [sym_string_literal] = ACTIONS(1), - [sym_char_literal] = ACTIONS(1), - [sym_integer_literal] = ACTIONS(1), - [sym_builtin] = ACTIONS(1), - [sym_builtin_type] = ACTIONS(1), - [sym_keyword] = ACTIONS(1), - [sym_identifier] = ACTIONS(1), - [sym_operator] = ACTIONS(1), - [sym_punctuation] = ACTIONS(1), - }, - [STATE(1)] = { - [sym_source_file] = STATE(13), - [sym_block_comment] = STATE(2), - [aux_sym_source_file_repeat1] = STATE(2), - [ts_builtin_sym_end] = ACTIONS(3), - [sym_line_comment] = ACTIONS(5), - [anon_sym_SLASH_STAR] = ACTIONS(7), - [sym_string_literal] = ACTIONS(5), - [sym_char_literal] = ACTIONS(5), - [sym_integer_literal] = ACTIONS(5), - [sym_builtin] = ACTIONS(5), - [sym_builtin_type] = ACTIONS(9), - [sym_keyword] = ACTIONS(5), - [sym_identifier] = ACTIONS(9), - [sym_operator] = ACTIONS(9), - [sym_punctuation] = ACTIONS(9), - }, - [STATE(2)] = { - [sym_block_comment] = STATE(3), - [aux_sym_source_file_repeat1] = STATE(3), - [ts_builtin_sym_end] = ACTIONS(11), - [sym_line_comment] = ACTIONS(13), - [anon_sym_SLASH_STAR] = ACTIONS(7), - [sym_string_literal] = ACTIONS(13), - [sym_char_literal] = ACTIONS(13), - [sym_integer_literal] = ACTIONS(13), - [sym_builtin] = ACTIONS(13), - [sym_builtin_type] = ACTIONS(15), - [sym_keyword] = ACTIONS(13), - [sym_identifier] = ACTIONS(15), - [sym_operator] = ACTIONS(15), - [sym_punctuation] = ACTIONS(15), - }, - [STATE(3)] = { - [sym_block_comment] = STATE(3), - [aux_sym_source_file_repeat1] = STATE(3), - [ts_builtin_sym_end] = ACTIONS(17), - [sym_line_comment] = ACTIONS(19), - [anon_sym_SLASH_STAR] = ACTIONS(22), - [sym_string_literal] = ACTIONS(19), - [sym_char_literal] = ACTIONS(19), - [sym_integer_literal] = ACTIONS(19), - [sym_builtin] = ACTIONS(19), - [sym_builtin_type] = ACTIONS(25), - [sym_keyword] = ACTIONS(19), - [sym_identifier] = ACTIONS(25), - [sym_operator] = ACTIONS(25), - [sym_punctuation] = ACTIONS(25), - }, - [STATE(4)] = { - [ts_builtin_sym_end] = ACTIONS(28), - [sym_line_comment] = ACTIONS(28), - [anon_sym_SLASH_STAR] = ACTIONS(28), - [sym_string_literal] = ACTIONS(28), - [sym_char_literal] = ACTIONS(28), - [sym_integer_literal] = ACTIONS(28), - [sym_builtin] = ACTIONS(28), - [sym_builtin_type] = ACTIONS(30), - [sym_keyword] = ACTIONS(28), - [sym_identifier] = ACTIONS(30), - [sym_operator] = ACTIONS(30), - [sym_punctuation] = ACTIONS(30), - }, - [STATE(5)] = { - [ts_builtin_sym_end] = ACTIONS(32), - [sym_line_comment] = ACTIONS(32), - [anon_sym_SLASH_STAR] = ACTIONS(32), - [sym_string_literal] = ACTIONS(32), - [sym_char_literal] = ACTIONS(32), - [sym_integer_literal] = ACTIONS(32), - [sym_builtin] = ACTIONS(32), - [sym_builtin_type] = ACTIONS(34), - [sym_keyword] = ACTIONS(32), - [sym_identifier] = ACTIONS(34), - [sym_operator] = ACTIONS(34), - [sym_punctuation] = ACTIONS(34), - }, -}; - -static const uint16_t ts_small_parse_table[] = { - [0] = 5, - ACTIONS(36), 1, - anon_sym_SLASH_STAR, - ACTIONS(38), 1, - aux_sym_block_comment_token1, - ACTIONS(42), 1, - anon_sym_STAR_SLASH, - ACTIONS(40), 2, - aux_sym_block_comment_token2, - aux_sym_block_comment_token3, - STATE(7), 2, - sym_block_comment, - aux_sym_block_comment_repeat1, - [18] = 5, - ACTIONS(36), 1, - anon_sym_SLASH_STAR, - ACTIONS(44), 1, - aux_sym_block_comment_token1, - ACTIONS(48), 1, - anon_sym_STAR_SLASH, - ACTIONS(46), 2, - aux_sym_block_comment_token2, - aux_sym_block_comment_token3, - STATE(8), 2, - sym_block_comment, - aux_sym_block_comment_repeat1, - [36] = 5, - ACTIONS(50), 1, - anon_sym_SLASH_STAR, - ACTIONS(53), 1, - aux_sym_block_comment_token1, - ACTIONS(59), 1, - anon_sym_STAR_SLASH, - ACTIONS(56), 2, - aux_sym_block_comment_token2, - aux_sym_block_comment_token3, - STATE(8), 2, - sym_block_comment, - aux_sym_block_comment_repeat1, - [54] = 5, - ACTIONS(36), 1, - anon_sym_SLASH_STAR, - ACTIONS(61), 1, - aux_sym_block_comment_token1, - ACTIONS(65), 1, - anon_sym_STAR_SLASH, - ACTIONS(63), 2, - aux_sym_block_comment_token2, - aux_sym_block_comment_token3, - STATE(10), 2, - sym_block_comment, - aux_sym_block_comment_repeat1, - [72] = 5, - ACTIONS(36), 1, - anon_sym_SLASH_STAR, - ACTIONS(44), 1, - aux_sym_block_comment_token1, - ACTIONS(67), 1, - anon_sym_STAR_SLASH, - ACTIONS(46), 2, - aux_sym_block_comment_token2, - aux_sym_block_comment_token3, - STATE(8), 2, - sym_block_comment, - aux_sym_block_comment_repeat1, - [90] = 2, - ACTIONS(28), 1, - aux_sym_block_comment_token1, - ACTIONS(30), 4, - anon_sym_SLASH_STAR, - aux_sym_block_comment_token2, - aux_sym_block_comment_token3, - anon_sym_STAR_SLASH, - [100] = 2, - ACTIONS(32), 1, - aux_sym_block_comment_token1, - ACTIONS(34), 4, - anon_sym_SLASH_STAR, - aux_sym_block_comment_token2, - aux_sym_block_comment_token3, - anon_sym_STAR_SLASH, - [110] = 1, - ACTIONS(69), 1, - ts_builtin_sym_end, -}; - -static const uint32_t ts_small_parse_table_map[] = { - [SMALL_STATE(6)] = 0, - [SMALL_STATE(7)] = 18, - [SMALL_STATE(8)] = 36, - [SMALL_STATE(9)] = 54, - [SMALL_STATE(10)] = 72, - [SMALL_STATE(11)] = 90, - [SMALL_STATE(12)] = 100, - [SMALL_STATE(13)] = 110, -}; - -static const TSParseActionEntry ts_parse_actions[] = { - [0] = {.entry = {.count = 0, .reusable = false}}, - [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(), - [3] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 0, 0, 0), - [5] = {.entry = {.count = 1, .reusable = true}}, SHIFT(2), - [7] = {.entry = {.count = 1, .reusable = true}}, SHIFT(6), - [9] = {.entry = {.count = 1, .reusable = false}}, SHIFT(2), - [11] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_source_file, 1, 0, 0), - [13] = {.entry = {.count = 1, .reusable = true}}, SHIFT(3), - [15] = {.entry = {.count = 1, .reusable = false}}, SHIFT(3), - [17] = {.entry = {.count = 1, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), - [19] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(3), - [22] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(6), - [25] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_source_file_repeat1, 2, 0, 0), SHIFT_REPEAT(3), - [28] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_block_comment, 2, 0, 0), - [30] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_block_comment, 2, 0, 0), - [32] = {.entry = {.count = 1, .reusable = true}}, REDUCE(sym_block_comment, 3, 0, 0), - [34] = {.entry = {.count = 1, .reusable = false}}, REDUCE(sym_block_comment, 3, 0, 0), - [36] = {.entry = {.count = 1, .reusable = false}}, SHIFT(9), - [38] = {.entry = {.count = 1, .reusable = true}}, SHIFT(7), - [40] = {.entry = {.count = 1, .reusable = false}}, SHIFT(7), - [42] = {.entry = {.count = 1, .reusable = false}}, SHIFT(4), - [44] = {.entry = {.count = 1, .reusable = true}}, SHIFT(8), - [46] = {.entry = {.count = 1, .reusable = false}}, SHIFT(8), - [48] = {.entry = {.count = 1, .reusable = false}}, SHIFT(5), - [50] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), SHIFT_REPEAT(9), - [53] = {.entry = {.count = 2, .reusable = true}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), SHIFT_REPEAT(8), - [56] = {.entry = {.count = 2, .reusable = false}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), SHIFT_REPEAT(8), - [59] = {.entry = {.count = 1, .reusable = false}}, REDUCE(aux_sym_block_comment_repeat1, 2, 0, 0), - [61] = {.entry = {.count = 1, .reusable = true}}, SHIFT(10), - [63] = {.entry = {.count = 1, .reusable = false}}, SHIFT(10), - [65] = {.entry = {.count = 1, .reusable = false}}, SHIFT(11), - [67] = {.entry = {.count = 1, .reusable = false}}, SHIFT(12), - [69] = {.entry = {.count = 1, .reusable = true}}, ACCEPT_INPUT(), -}; - -#ifdef __cplusplus -extern "C" { -#endif -#ifdef TREE_SITTER_HIDE_SYMBOLS -#define TS_PUBLIC -#elif defined(_WIN32) -#define TS_PUBLIC __declspec(dllexport) -#else -#define TS_PUBLIC __attribute__((visibility("default"))) -#endif - -TS_PUBLIC const TSLanguage *tree_sitter_ferro(void) { - static const TSLanguage language = { - .abi_version = LANGUAGE_VERSION, - .symbol_count = SYMBOL_COUNT, - .alias_count = ALIAS_COUNT, - .token_count = TOKEN_COUNT, - .external_token_count = EXTERNAL_TOKEN_COUNT, - .state_count = STATE_COUNT, - .large_state_count = LARGE_STATE_COUNT, - .production_id_count = PRODUCTION_ID_COUNT, - .field_count = FIELD_COUNT, - .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH, - .parse_table = &ts_parse_table[0][0], - .small_parse_table = ts_small_parse_table, - .small_parse_table_map = ts_small_parse_table_map, - .parse_actions = ts_parse_actions, - .symbol_names = ts_symbol_names, - .symbol_metadata = ts_symbol_metadata, - .public_symbol_map = ts_symbol_map, - .alias_map = ts_non_terminal_alias_map, - .alias_sequences = &ts_alias_sequences[0][0], - .lex_modes = (const void*)ts_lex_modes, - .lex_fn = ts_lex, - .primary_state_ids = ts_primary_state_ids, - }; - return &language; -} -#ifdef __cplusplus -} -#endif diff --git a/src/tree_sitter/parser.h b/src/tree_sitter/parser.h deleted file mode 100644 index 5f37997..0000000 --- a/src/tree_sitter/parser.h +++ /dev/null @@ -1,55 +0,0 @@ -#ifndef TREE_SITTER_PARSER_H_ -#define TREE_SITTER_PARSER_H_ -#ifdef __cplusplus -extern "C" { -#endif -#include -#include -#include -#define ts_builtin_sym_error ((TSSymbol)-1) -#define ts_builtin_sym_end 0 -#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 -#ifndef TREE_SITTER_API_H_ -typedef uint16_t TSStateId; -typedef uint16_t TSSymbol; -typedef uint16_t TSFieldId; -typedef struct TSLanguage TSLanguage; -typedef struct TSLanguageMetadata { uint8_t major_version; uint8_t minor_version; uint8_t patch_version; } TSLanguageMetadata; -#endif -typedef struct { TSFieldId field_id; uint8_t child_index; bool inherited; } TSFieldMapEntry; -typedef struct { uint16_t index; uint16_t length; } TSMapSlice; -typedef struct { bool visible; bool named; bool supertype; } TSSymbolMetadata; -typedef struct TSLexer TSLexer; -struct TSLexer { int32_t lookahead; TSSymbol result_symbol; void (*advance)(TSLexer *, bool); void (*mark_end)(TSLexer *); uint32_t (*get_column)(TSLexer *); bool (*is_at_included_range_start)(const TSLexer *); bool (*eof)(const TSLexer *); void (*log)(const TSLexer *, const char *, ...); }; -typedef enum { TSParseActionTypeShift, TSParseActionTypeReduce, TSParseActionTypeAccept, TSParseActionTypeRecover } TSParseActionType; -typedef union { struct { uint8_t type; TSStateId state; bool extra; bool repetition; } shift; struct { uint8_t type; uint8_t child_count; TSSymbol symbol; int16_t dynamic_precedence; uint16_t production_id; } reduce; uint8_t type; } TSParseAction; -typedef struct { uint16_t lex_state; uint16_t external_lex_state; } TSLexMode; -typedef struct { uint16_t lex_state; uint16_t external_lex_state; uint16_t reserved_word_set_id; } TSLexerMode; -typedef union { TSParseAction action; struct { uint8_t count; bool reusable; } entry; } TSParseActionEntry; -typedef struct { int32_t start; int32_t end; } TSCharacterRange; -struct TSLanguage { uint32_t abi_version; uint32_t symbol_count; uint32_t alias_count; uint32_t token_count; uint32_t external_token_count; uint32_t state_count; uint32_t large_state_count; uint32_t production_id_count; uint32_t field_count; uint16_t max_alias_sequence_length; const uint16_t *parse_table; const uint16_t *small_parse_table; const uint32_t *small_parse_table_map; const TSParseActionEntry *parse_actions; const char * const *symbol_names; const char * const *field_names; const TSMapSlice *field_map_slices; const TSFieldMapEntry *field_map_entries; const TSSymbolMetadata *symbol_metadata; const TSSymbol *public_symbol_map; const uint16_t *alias_map; const TSSymbol *alias_sequences; const TSLexerMode *lex_modes; bool (*lex_fn)(TSLexer *, TSStateId); bool (*keyword_lex_fn)(TSLexer *, TSStateId); TSSymbol keyword_capture_token; struct { const bool *states; const TSSymbol *symbol_map; void *(*create)(void); void (*destroy)(void *); bool (*scan)(void *, TSLexer *, const bool *); unsigned (*serialize)(void *, char *); void (*deserialize)(void *, const char *, unsigned); } external_scanner; const TSStateId *primary_state_ids; const char *name; const TSSymbol *reserved_words; uint16_t max_reserved_word_set_size; uint32_t supertype_count; const TSSymbol *supertype_symbols; const TSMapSlice *supertype_map_slices; const TSSymbol *supertype_map_entries; TSLanguageMetadata metadata; }; -static inline bool set_contains(const TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { uint32_t index=0,size=len; while(size>1){uint32_t half=size/2,mid=index+half; const TSCharacterRange *r=&ranges[mid]; if(lookahead>=r->start&&lookahead<=r->end)return true; else if(lookahead>r->end)index=mid; size-=half;} const TSCharacterRange *r=&ranges[index]; return lookahead>=r->start&&lookahead<=r->end; } -#ifdef _MSC_VER -#define UNUSED __pragma(warning(suppress : 4101)) -#else -#define UNUSED __attribute__((unused)) -#endif -#define START_LEXER() bool result=false; bool skip=false; UNUSED bool eof=false; int32_t lookahead; goto start; next_state: lexer->advance(lexer,skip); start: skip=false; lookahead=lexer->lookahead; -#define ADVANCE(state_value) { state=state_value; goto next_state; } -#define ADVANCE_MAP(...) { static const uint16_t map[]={__VA_ARGS__}; for(uint32_t i=0;iresult_symbol=symbol_value; lexer->mark_end(lexer); -#define END_STATE() return result; -#define SMALL_STATE(id) ((id)-LARGE_STATE_COUNT) -#define STATE(id) id -#define ACTIONS(id) id -#define SHIFT(state_value) {{.shift={.type=TSParseActionTypeShift,.state=(state_value)}}} -#define SHIFT_REPEAT(state_value) {{.shift={.type=TSParseActionTypeShift,.state=(state_value),.repetition=true}}} -#define SHIFT_EXTRA() {{.shift={.type=TSParseActionTypeShift,.extra=true}}} -#define REDUCE(symbol_name,children,precedence,prod_id) {{.reduce={.type=TSParseActionTypeReduce,.symbol=symbol_name,.child_count=children,.dynamic_precedence=precedence,.production_id=prod_id},}} -#define RECOVER() {{.type=TSParseActionTypeRecover}} -#define ACCEPT_INPUT() {{.type=TSParseActionTypeAccept}} -#ifdef __cplusplus -} -#endif -#endif diff --git a/tests/run.py b/tests/run.py new file mode 100644 index 0000000..20700dd --- /dev/null +++ b/tests/run.py @@ -0,0 +1,140 @@ +"""Run every fixture through the front end and check what it reports. + +The compiler is a front end now -- lexer, parser, types, ownership -- so a +fixture is checked by running `fec` on it and looking at two things: whether it +was accepted, and, when it was rejected, whether the diagnostic is the one the +fixture asked for. + +A fixture states its expectation in its first line: + + // ERROR:8:self rejected at line 8, with "self" in the message + // ERROR:expected ';' rejected, message only -- the parse fixtures, where + the line is not the interesting part + +A fixture with no marker whose name starts with `bad` must be rejected but does +not pin the message yet. Anything else must be accepted. + +Fixtures under `parse/` are checked with --dump-ast rather than --check: they +exercise the grammar, and several are deliberately not well-typed. + +This runs on the host in about a second. There is no VM: nothing here executes +generated code, because there is no code generator. +""" +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +FIXTURES = ROOT / "fec" / "tests" +WATCOM = ROOT / ".dosboxx" / "watcom" +SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", + "check", "driver") +# Fixtures live here until there is a code generator to run them against. +QUARANTINE = "pending-backend" + +MARKER = re.compile(r"^//\s*ERROR:(?:(\d+):)?(.*)$") + + +@dataclass +class Expectation: + rejected: bool + line: int | None = None + text: str | None = None + + +def expectation(path: Path) -> Expectation: + first = path.read_text(encoding="utf-8", errors="replace").split("\n", 1)[0] + m = MARKER.match(first.strip()) + if m: + line = int(m.group(1)) if m.group(1) else None + return Expectation(True, line, m.group(2).strip()) + name = path.stem + return Expectation(name.startswith("bad") or "-bad-" in name or name.startswith("own-bad")) + + +def build(out: Path) -> Path: + """Build the front end with the pinned toolchain, hosted.""" + wcl = WATCOM / "binnt" / "wcl386.exe" + if not wcl.is_file(): + sys.exit(f"pinned Open Watcom not found at {WATCOM}") + out.mkdir(parents=True, exist_ok=True) + env = dict(os.environ) + env.update(WATCOM=str(WATCOM), INCLUDE=f"{WATCOM / 'h'};{WATCOM / 'h' / 'nt'}", + PATH=f"{WATCOM / 'binnt'}{os.pathsep}{env.get('PATH', '')}") + src = ROOT / "fec" / "src" + cmd = [str(wcl), "-q", "-za", "-wx", "-bt=nt", "-fe=fec.exe", f"-i={src}"] + cmd += [str(src / f"{n}.c") for n in SOURCES] + done = subprocess.run(cmd, cwd=out, capture_output=True, text=True, env=env) + if done.returncode != 0 or (done.stdout + done.stderr).strip(): + sys.exit("front end does not build clean:\n" + done.stdout + done.stderr) + return out / "fec.exe" + + +def run_case(fec: Path, path: Path) -> tuple[bool, str]: + want = expectation(path) + # The grammar fixtures are not all well-typed; stop after parsing. + mode = "--dump-ast" if path.parent.name == "parse" else "--check" + done = subprocess.run([str(fec), mode, str(path)], + capture_output=True, text=True, timeout=30) + output = (done.stdout + done.stderr).strip() + rejected = done.returncode != 0 + + if want.rejected != rejected: + verb = "accepted" if rejected else "rejected" + return False, f"expected to be {'rejected' if want.rejected else verb}" + if not want.rejected: + return True, "" + if want.line is None: + if want.text and want.text.lower() not in output.lower(): + got = output.splitlines()[0] if output else "(silent)" + return False, f"marker wants {want.text!r}\n {got}" + return True, "" + # The marker pins where and roughly what, so a rule can be moved or reworded + # only deliberately. + first = output.split("\n", 1)[0] if output else "" + at = re.search(r":(\d+):\d+: error:", first) + if not at: + return False, f"no diagnostic to match marker\n got: {first or '(silent)'}" + if int(at.group(1)) != want.line: + return False, f"marker says line {want.line}, diagnostic is line {at.group(1)}\n {first}" + if want.text and want.text.lower() not in output.lower(): + return False, f"marker wants {want.text!r}\n {first}" + return True, "" + + +def main() -> int: + ap = argparse.ArgumentParser(description="run the front-end fixtures") + ap.add_argument("-k", dest="select", help="only fixtures whose path contains this") + ap.add_argument("-v", dest="verbose", action="store_true") + args = ap.parse_args() + + fec = build(ROOT / ".build") + cases = sorted(p for p in FIXTURES.rglob("*.fe") if QUARANTINE not in p.parts) + if args.select: + cases = [p for p in cases if args.select in p.as_posix()] + + failed = [] + for path in cases: + ok, why = run_case(fec, path) + rel = path.relative_to(FIXTURES).as_posix() + if ok: + if args.verbose: + print(f" ok {rel}") + else: + failed.append((rel, why)) + for rel, why in failed: + print(f"FAIL {rel}: {why}") + marked = sum(1 for p in cases if expectation(p).line is not None) + print(f"\n{len(cases) - len(failed)}/{len(cases)} passed " + f"({marked} pin a line and message)") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/README.md b/tools/README.md deleted file mode 100644 index 97b141e..0000000 --- a/tools/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Development tools - -## Host support - -The automated DOS development environment currently supports Windows 10/11. -The host only needs `uv`. The setup command downloads the pinned DOSBox-X and -Open Watcom DOS archives, verifies their SHA-256 hashes, and installs them in the -ignored `.dosboxx/` cache. - -```powershell -uv run ferro-dos setup --accept-watcom-license -``` - -Review the Open Watcom license referenced by -`tools/toolchains/dosboxx.lock.json` before accepting it. Neither downloaded -archives nor installed tools are committed. - -## General DOS environment - -`ferro-dos` provides the development entry points: - -```powershell -uv run ferro-dos build -uv run ferro-dos exec "FEC.EXE --check TESTS\M6\OKLAST.FE" -uv run ferro-dos batch fec\test-dos.bat -uv run ferro-dos shell -uv run ferro-dos --help -``` - -Every invocation creates an isolated host directory under `.dosboxx/runs/` and -mounts it as writable `C:`. The repository is mounted read-only as `R:` and the -pinned Open Watcom installation as read-only `W:`. Current compiler sources, -the standard library, and fixtures are copied to `C:\FEC`; all compilation and -execution happen there inside DOSBox-X. Successful runs are removed by default. -Use `--keep` to retain a workspace and `--show-dos` to display the DOS window. - -This directory-backed layout deliberately has no QEMU, disk-image, TCP-agent, -or OCR dependency. A future disk-image backend can be added without changing -the command interface. - -## Pytest regression suite - -`ferro-test` uses the same isolated DOSBox-X/Open Watcom environment, builds -`FEC.EXE` once, and executes all selected cases sequentially in that one DOS -instance. Pytest still reports each registered case separately. - -```powershell -uv run ferro-test run --through m6 -v -uv run ferro-test run --only m6 --dos-log -uv run ferro-test --help -``` - -`--keep-failed` preserves a failed workspace, `--dos-log` prints captured DOS -output, `--trace-dos` disables per-command redirection, and `--show-dos` displays -the GUI. Working rules and DOS/Open Watcom build traps are in `AGENTS.md`. diff --git a/tools/tests/test_dos_names.py b/tools/tests/test_dos_names.py deleted file mode 100644 index 2daf1d5..0000000 --- a/tools/tests/test_dos_names.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Host-side checks for constraints the DOS toolchain enforces far too late. - -Everything here runs without DOSBox-X. The point is to fail in a tenth of a -second with the offending name, instead of after a DOSBox-X boot and ten -successful object builds -- and with a message that says what is actually wrong. -A 9-character source name reaches the DOS build as ``Unable to open "src\\x.c"``, -which reads as a missing file rather than a name that cannot be represented. -""" -from __future__ import annotations - -from pathlib import Path - -import pytest - -from ferrolang_vm.paths import ROOT -from ferrolang_vm.registry import CASES - -# The runner copies these onto a FAT filesystem, where a name is at most eight -# characters plus a three-character extension. -COPIED_TREES = ("fec/src", "fec/std", "fec/tests") - - -def _offenders(root: Path) -> list[str]: - bad = [] - for path in sorted(root.rglob("*")): - name = path.name - if name.startswith("."): - continue - stem, _, suffix = name.rpartition(".") if "." in name else (name, "", "") - if len(stem) > 8 or len(suffix) > 3: - bad.append(f"{path.relative_to(ROOT).as_posix()} (stem {len(stem)}, ext {len(suffix)})") - return bad - - -@pytest.mark.parametrize("tree", COPIED_TREES) -def test_copied_files_fit_dos_8_3(tree: str) -> None: - root = ROOT / tree - if not root.is_dir(): - pytest.skip(f"{tree} is absent") - bad = _offenders(root) - assert not bad, ( - f"{len(bad)} name(s) under {tree} cannot be represented on the DOS side.\n" - "The DOS build will report them as missing files, not as long names:\n " - + "\n ".join(bad) - ) - - -def test_registry_paths_exist_on_the_host() -> None: - """Every ``.FE`` a case names must exist, matched case-insensitively. - - DOS is case-insensitive, so a registry typo survives until the command runs - inside the VM and fails with a message about the wrong thing. - """ - available = { - path.relative_to(ROOT / "fec").as_posix().upper() - for path in (ROOT / "fec").rglob("*.fe") - } - available |= { - path.relative_to(ROOT / "fec").as_posix().upper() - for path in (ROOT / "fec").rglob("*.FE") - } - missing = [] - for case in CASES: - for token in case.command.split(): - if not token.upper().endswith(".FE"): - continue - wanted = token.replace("\\", "/").upper() - if wanted.startswith("STD/"): - wanted = f"STD/{wanted[4:]}" - if not any(entry.endswith(wanted) for entry in available): - missing.append(f"{case.id}: {token}") - assert not missing, "registry names fixtures that do not exist:\n " + "\n ".join(missing) diff --git a/tools/tests/test_host_syntax.py b/tools/tests/test_host_syntax.py deleted file mode 100644 index 69a3e2b..0000000 --- a/tools/tests/test_host_syntax.py +++ /dev/null @@ -1,113 +0,0 @@ -"""Compile the compiler's own sources on the host, as a syntax gate. - -This is not verification. AGENTS.md is explicit that a host compiler's result -does not count, and it still does not: the DOS build and the milestone suite -decide whether anything works. What this buys is the turnaround. A missing -declaration or a signature that disagrees with its definition used to surface -only after a DOSBox-X boot and a full compiler build; here it surfaces in about -a second, with the line number. - -It runs the pinned toolchain's Windows-hosted 16-bit driver with the exact -command ``fec/build-dos.bat`` uses, so the diagnostics match what the DOS build -sees. A 32-bit compile is not equivalent: it misses warnings that only the -16-bit large model reports, which is how a dead function survived the M7 -unification with a clean 32-bit check. - -What it still cannot see is the DOS environment itself -- memory limits, the -command line length, the filesystem. The DOS build and the milestone suite -remain the gate. - -It uses the pinned toolchain only. There is no environment override and no -skip: a system-wide Watcom is a different version reporting different things, -and a gate that quietly skips is not a gate. -""" -from __future__ import annotations - -import os -import re -import subprocess -from pathlib import Path - -import pytest - -from ferrolang_vm.paths import ROOT - -SRC = ROOT / "fec" / "src" -# Mirrors the compile order in fec/build-dos.bat. -SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", - "check", "lower", "emit_c", "driver") - - -@pytest.fixture(scope="session") -def watcom() -> Path: - """The pinned toolchain, and nothing else. - - Deliberately no environment override and no skip. A system-wide Open Watcom - is a different version with different diagnostics, and a gate that skips is - a gate that is not running -- which is the failure mode this file exists to - close. dosboxx.py fails the same way when the toolchain is missing. - """ - base = ROOT / ".dosboxx" / "watcom" - if not (base / "binnt" / "wcl.exe").is_file(): - raise AssertionError( - f"the pinned Open Watcom is not at {base}; " - "run `uv run ferro-dos setup --accept-watcom-license`") - return base - - -@pytest.fixture(scope="session") -def objdir(tmp_path_factory: pytest.TempPathFactory) -> Path: - return tmp_path_factory.mktemp("wcc") - - -@pytest.mark.parametrize("name", SOURCES) -def test_source_compiles_clean(name: str, watcom: Path, objdir: Path) -> None: - source = SRC / f"{name}.c" - if not source.is_file(): - pytest.fail(f"{source} is missing but build-dos.bat compiles it") - env = dict(os.environ) - env["WATCOM"] = os.fspath(watcom) - env["INCLUDE"] = os.fspath(watcom / "h") - env["PATH"] = os.pathsep.join( - [os.fspath(watcom / "binnt"), env.get("PATH", "")]) - # The same command build-dos.bat runs, minus the object name. - completed = subprocess.run( - [os.fspath(watcom / "binnt" / "wcl.exe"), "-q", "-za", "-wx", - "-bt=dos", "-ml", "-k32768", "-c", f"-i={SRC}", os.fspath(source)], - cwd=objdir, capture_output=True, text=True, env=env, timeout=120, - ) - output = (completed.stdout + completed.stderr).strip() - # -wx keeps warnings meaningful, so treat any diagnostic as a failure. The - # DOS build prints them to a screen nobody reads, which is how they - # accumulate unnoticed. - assert completed.returncode == 0 and not output, ( - f"{name}.c does not compile clean\n{output}" - ) - - -def test_build_scripts_agree_on_sources() -> None: - """The two build files and this test must name the same translation units. - - They drifted apart while the M7 wrapper existed, which is how a source could - stop being compiled without anyone noticing. - """ - batch = (ROOT / "fec" / "build-dos.bat").read_text(encoding="utf-8", errors="replace") - makefile = (ROOT / "fec" / "Makefile").read_text(encoding="utf-8", errors="replace") - in_batch = set(re.findall(r"src\\(\w+)\.c", batch)) - srcline = next(line for line in makefile.splitlines() if line.startswith("SRC =")) - in_make = set(re.findall(r"src/(\w+)\.c", srcline)) - assert in_batch == set(SOURCES), f"build-dos.bat compiles {sorted(in_batch)}" - assert in_make == set(SOURCES), f"Makefile compiles {sorted(in_make)}" - - -def test_no_source_is_orphaned() -> None: - """Every .c under fec/src must be compiled by something. - - check_m7.c and emitcm7.c hid check.c and emit_c.c from the build by - including them textually; nothing flagged that they had stopped being - translation units of their own. - """ - on_disk = {p.stem for p in SRC.glob("*.c")} - assert on_disk == set(SOURCES), ( - f"fec/src has {sorted(on_disk - set(SOURCES))} that no build step compiles" - ) diff --git a/tools/tests/test_milestones_dosboxx.py b/tools/tests/test_milestones_dosboxx.py deleted file mode 100644 index 8b8ae3f..0000000 --- a/tools/tests/test_milestones_dosboxx.py +++ /dev/null @@ -1,55 +0,0 @@ -from __future__ import annotations - -import os -import warnings - -import pytest - -from ferrolang_vm.dosboxx import SuiteRun, run_suite -from ferrolang_vm.registry import MAX_MILESTONE, all_cases, milestone_number -from ferrolang_vm.suite import Case - -ONLY = os.environ.get("FERRO_TEST_ONLY") -CASES = all_cases( - through=milestone_number(os.environ.get("FERRO_TEST_THROUGH", f"m{MAX_MILESTONE}")), - only=milestone_number(ONLY) if ONLY else None, -) - - -@pytest.fixture(scope="session") -def suite_run() -> SuiteRun: - run = run_suite( - CASES, - keep=os.environ.get("FERRO_TEST_KEEP_FAILED") == "1", - show_dos=os.environ.get("FERRO_TEST_SHOW_DOS") == "1", - trace_dos=os.environ.get("FERRO_TEST_TRACE_DOS") == "1", - ) - yield run - if os.environ.get("FERRO_TEST_DOS_LOG") == "1": - console = run.root / "CONSOLE.LOG" - if console.is_file(): - print(console.read_text(encoding="utf-8", errors="replace")) - run.cleanup() - - -def test_compiler_build(suite_run: SuiteRun) -> None: - assert suite_run.result() == "PASS", suite_run.log() - - -@pytest.mark.parametrize("case", CASES, ids=lambda case: case.id) -def test_milestone_case(case: Case, suite_run: SuiteRun) -> None: - if suite_run.result() != "PASS": - pytest.skip("compiler build failed") - result = suite_run.result(case) - err = suite_run.err(case) - if result == "PASS" and err: - warning_lines = [line for line in err.splitlines() if "warning" in line.lower()] - if warning_lines: - warnings.warn("\n".join(warning_lines), stacklevel=1) - code = suite_run.rc(case) - assert result == "PASS", ( - f"DOS command: {case.command}\n" - f"Expected success: {case.expect_success}\n" - f"Exit code: {'not recorded' if code is None else code}\n" - f"{suite_run.log(case)}\n{err}" - ) diff --git a/tools/toolchains/dosboxx.lock.json b/tools/toolchains/dosboxx.lock.json deleted file mode 100644 index 08b1123..0000000 --- a/tools/toolchains/dosboxx.lock.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "schema": 1, - "dosboxx": { - "version": "2026.08.02", - "url": "https://github.com/joncampbell123/dosbox-x/releases/download/dosbox-x-v2026.08.02/dosbox-x-vsbuild-win64-2026.08.02-portable.zip", - "sha256": "ca28208f5fee25a74caf3a02cc0189c7f7943a42ce37c02848f5fc03450a96cf", - "executable": "bin/x64/Release/dosbox-x.exe" - }, - "open_watcom": { - "version": "2026-08-01-Build", - "url": "https://github.com/open-watcom/open-watcom-v2/releases/download/2026-08-01-Build/open-watcom-2_0-c-dos.exe", - "sha256": "80db4ab340f382e59bf3d396280576ec837964c2ef00e8ddd3b2b3724ab63edf", - "required": [ - "binw/wcl.exe", - "binw/wcl386.exe", - "binp/wlink.exe", - "h/stdio.h", - "lib286/dos/clibl.lib", - "lib386/dos/clib3r.lib", - "license.txt" - ] - } -} From 547d8c5ec29ba1bdba2c0c0b7e1b8a4ea997735a Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 03:42:06 +0900 Subject: [PATCH 112/184] fix: reconcile the ERROR markers with what the checker reports Checking the markers for the first time found five disagreements in areas that are implemented. Four were the marker's fault: - own/badarg pinned "self", but the rule being broken is that a returned reference must derive from a parameter -- `self` has nothing to do with it. - own/badbrmov pinned line 9, which is the closing brace; the second destroy is on line 8. - own/badloop pinned line 8, the destroy after the loop. The diagnostic is on line 6, inside it, and line 6 is right: the second iteration moves the same value again, so the loop body is where it is caught. Whoever wrote the marker expected the error after the loop. - optional/badcatch pinned line 14, the body of the catch block. The catch expression on line 13 is what cannot fall through. The fifth was the compiler's. own/badweak assigns a `&mut i32` to a `&i32` and got "initializer type mismatch", which says nothing about why. Weakening an exclusive borrow to a shared one is a specific rule and now says so, for references and slices alike. own/ is fully green: 50/50. Overall 133 -> 138 of 188. The six remaining marker disagreements are all under units/ and generic/, where nothing is implemented yet, so there is no diagnostic to compare against and no way to tell whether the marker is right. --- fec/src/check.c | 18 ++++++++++++++++-- fec/tests/optional/badcatch.fe | 2 +- fec/tests/own/badarg.fe | 2 +- fec/tests/own/badbrmov.fe | 2 +- fec/tests/own/badloop.fe | 2 +- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index 141d443..37e4370 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -2489,8 +2489,22 @@ static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) if (expected && expected->kind==FE_TYPE_VOID) err(s->c,n->loc,"variable cannot have void type"); if (n->b && !fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->b)) - err(s->c,n->loc,"initializer type mismatch"); + !m7_actual_compatible(expected,stored,n->b)) { + /* Say which rule was hit. Weakening &mut to & is a distinct thing from + two unrelated types not matching, and "type mismatch" told the reader + nothing about why the exclusive borrow could not be shared. */ + if (expected && stored && expected->kind==FE_TYPE_REF && + stored->kind==FE_TYPE_REF && !expected->ref_mut && stored->ref_mut) + err(s->c,n->loc, + "cannot rebind a mut borrow as a shared reference"); + else if (expected && stored && expected->kind==FE_TYPE_SLICE && + stored->kind==FE_TYPE_SLICE && !expected->ref_mut && + stored->ref_mut) + err(s->c,n->loc, + "cannot rebind a mut slice as a shared slice"); + else + err(s->c,n->loc,"initializer type mismatch"); + } /* Rules the M6 declaration case carried that this one has to repeat now that it is the only declaration case. */ if (n->b && stored && stored->kind==FE_TYPE_VOID) diff --git a/fec/tests/optional/badcatch.fe b/fec/tests/optional/badcatch.fe index d6b2bdb..97ed75c 100644 --- a/fec/tests/optional/badcatch.fe +++ b/fec/tests/optional/badcatch.fe @@ -1,4 +1,4 @@ -// ERROR:14:catch +// ERROR:13:catch unit badcatch; error E { diff --git a/fec/tests/own/badarg.fe b/fec/tests/own/badarg.fe index 355fb9c..7e50114 100644 --- a/fec/tests/own/badarg.fe +++ b/fec/tests/own/badarg.fe @@ -1,4 +1,4 @@ -// ERROR:8:self +// ERROR:8:derived from a parameter unit badarg; struct Box { diff --git a/fec/tests/own/badbrmov.fe b/fec/tests/own/badbrmov.fe index 7dbb11b..f07825d 100644 --- a/fec/tests/own/badbrmov.fe +++ b/fec/tests/own/badbrmov.fe @@ -1,4 +1,4 @@ -// ERROR:9:move +// ERROR:8:move unit badbrmov; fn bad(p: ^i32, consume: bool) -> void { diff --git a/fec/tests/own/badloop.fe b/fec/tests/own/badloop.fe index fed8c0f..1cb6a4f 100644 --- a/fec/tests/own/badloop.fe +++ b/fec/tests/own/badloop.fe @@ -1,4 +1,4 @@ -// ERROR:8:move +// ERROR:6:move unit badloop; fn bad(p: ^i32, again: bool) -> void { From 51e2568ba7e23af203d5be71fcb2130558936689 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 03:47:28 +0900 Subject: [PATCH 113/184] implement: unit identity -- dotted paths, name rules, source path Units start here, with the part that needs no import graph: what a unit is called and where it must live. The parser only ever read a single identifier after `unit` and `import`, so `unit game.main;` and `import std.io;` were syntax errors -- which is why the dotted fixtures failed at the semicolon. It now reads a dotted path and stores it canonically, dots included, since that spelling is the unit's identity everywhere else. `import a.b as c;` parses too, with the alias on the node. resolve.c is the new pass between parsing and checking, for the questions that span files. It carries SPEC 8.1 so far: each path segment is ASCII lowercase, starts with a letter, continues with letters, digits or underscore, and is at most eight characters; and the dotted path must match the source path it was read from, so game.world.map has to come from game/world/map.fe. The source side is folded to lowercase before comparing, because a case-insensitive host must not let two spellings become two units. That rule then applied to the fixtures, which were not obeying it: 57 declared a unit name unrelated to their file, left over from the milestone directories, and eight had names too long to be legal. Both are now aligned -- the rule is worth having only if the tree follows it. units: badupper, badlong and unitbad pass. 138 -> 146 of 188. The rest of units/ needs the import graph, which is the next piece: resolution, cycles, bindings and visibility. --- fec/src/driver.c | 10 +++ fec/src/parser.c | 40 ++++++++- fec/src/resolve.c | 86 +++++++++++++++++++ fec/src/resolve.h | 24 ++++++ fec/tests/format/{bad-ari.fe => bad_ari.fe} | 2 +- fec/tests/format/{bad-bufw.fe => bad_bufw.fe} | 2 +- fec/tests/format/{bad-cls.fe => bad_cls.fe} | 2 +- fec/tests/format/{bad-many.fe => bad_many.fe} | 2 +- fec/tests/format/{bad-open.fe => bad_open.fe} | 2 +- fec/tests/format/{bad-run.fe => bad_run.fe} | 2 +- fec/tests/format/{bad-try.fe => bad_try.fe} | 2 +- fec/tests/format/{bad-type.fe => bad_type.fe} | 2 +- fec/tests/format/{bad-verb.fe => bad_verb.fe} | 2 +- fec/tests/format/{bad-writ.fe => bad_writ.fe} | 2 +- .../format/{ok-format.fe => ok_forma.fe} | 2 +- fec/tests/format/{ok-prop.fe => ok_prop.fe} | 2 +- .../format/{ok-try-fpr.fe => ok_try_f.fe} | 2 +- .../own/{own-bad-clos.fe => bad_clos.fe} | 2 +- .../own/{own-bad-cond.fe => bad_cond.fe} | 2 +- fec/tests/own/{own-bad-dbl.fe => bad_dbl.fe} | 2 +- .../own/{own-bad-dest.fe => bad_dest.fe} | 2 +- .../own/{own-bad-drop.fe => bad_drop.fe} | 2 +- .../own/{own-bad-loop.fe => bad_loop.fe} | 2 +- .../own/{own-bad-move.fe => bad_move.fe} | 2 +- .../own/{own-bad-proj.fe => bad_proj.fe} | 2 +- fec/tests/own/{ok-defer.fe => ok_defer.fe} | 2 +- fec/tests/own/{ok-owned.fe => ok_owned.fe} | 2 +- fec/tests/parse/keybuilt.fe | 2 +- fec/tests/parse/logical.fe | 2 +- fec/tests/parse/misssemi.fe | 2 +- fec/tests/parse/unclcomm.fe | 2 +- fec/tests/parse/v012form.fe | 2 +- .../{bounds-nocheck.fe => bounds_nocheck.fe} | 0 .../{bounds-trap.fe => bounds_trap.fe} | 0 .../{ownership-drop.fe => ownership_drop.fe} | 0 ...ce-bounds-trap.fe => slice_bounds_trap.fe} | 0 fec/tests/types/{bad-ari.fe => bad_ari.fe} | 2 +- fec/tests/types/{bad-asgn.fe => bad_asgn.fe} | 2 +- fec/tests/types/{bad-cast.fe => bad_cast.fe} | 0 fec/tests/types/{bad-cond.fe => bad_cond.fe} | 2 +- fec/tests/types/{bad-mlet.fe => bad_mlet.fe} | 2 +- fec/tests/types/{bad-ret.fe => bad_ret.fe} | 2 +- fec/tests/types/{bad-shwr.fe => bad_shwr.fe} | 2 +- fec/tests/types/{bad-type.fe => bad_type.fe} | 2 +- fec/tests/types/{bad-unit.fe => bad_unit.fe} | 2 +- fec/tests/types/{bad-unk.fe => bad_unk.fe} | 2 +- fec/tests/types/{bad-void.fe => bad_void.fe} | 0 fec/tests/types/badarr.fe | 2 +- fec/tests/types/badchar.fe | 2 +- fec/tests/types/badcycle.fe | 2 +- fec/tests/types/badfield.fe | 2 +- fec/tests/types/badfld.fe | 2 +- fec/tests/types/badindex.fe | 2 +- fec/tests/types/badmat.fe | 2 +- fec/tests/types/badstr.fe | 2 +- .../types/{ok-arrayctx.fe => ok_arra1.fe} | 2 +- fec/tests/types/{ok-array.fe => ok_array.fe} | 2 +- .../types/{ok-castwhil.fe => ok_castw.fe} | 2 +- fec/tests/types/{ok-char.fe => ok_char.fe} | 2 +- fec/tests/types/{ok-enum.fe => ok_enum.fe} | 2 +- fec/tests/types/{ok-for.fe => ok_for.fe} | 2 +- fec/tests/types/{ok-hello.fe => ok_hello.fe} | 2 +- .../types/{ok-mutable.fe => ok_mutab.fe} | 2 +- fec/tests/types/{ok-nested.fe => ok_neste.fe} | 2 +- fec/tests/types/{ok-scopes.fe => ok_scope.fe} | 2 +- fec/tests/types/{ok-str.fe => ok_str.fe} | 2 +- fec/tests/types/{ok-struct.fe => ok_struc.fe} | 2 +- tests/run.py | 2 +- 68 files changed, 215 insertions(+), 61 deletions(-) create mode 100644 fec/src/resolve.c create mode 100644 fec/src/resolve.h rename fec/tests/format/{bad-ari.fe => bad_ari.fe} (75%) rename fec/tests/format/{bad-bufw.fe => bad_bufw.fe} (82%) rename fec/tests/format/{bad-cls.fe => bad_cls.fe} (76%) rename fec/tests/format/{bad-many.fe => bad_many.fe} (76%) rename fec/tests/format/{bad-open.fe => bad_open.fe} (75%) rename fec/tests/format/{bad-run.fe => bad_run.fe} (79%) rename fec/tests/format/{bad-try.fe => bad_try.fe} (77%) rename fec/tests/format/{bad-type.fe => bad_type.fe} (86%) rename fec/tests/format/{bad-verb.fe => bad_verb.fe} (76%) rename fec/tests/format/{bad-writ.fe => bad_writ.fe} (79%) rename fec/tests/format/{ok-format.fe => ok_forma.fe} (98%) rename fec/tests/format/{ok-prop.fe => ok_prop.fe} (84%) rename fec/tests/format/{ok-try-fpr.fe => ok_try_f.fe} (88%) rename fec/tests/own/{own-bad-clos.fe => bad_clos.fe} (89%) rename fec/tests/own/{own-bad-cond.fe => bad_cond.fe} (83%) rename fec/tests/own/{own-bad-dbl.fe => bad_dbl.fe} (77%) rename fec/tests/own/{own-bad-dest.fe => bad_dest.fe} (69%) rename fec/tests/own/{own-bad-drop.fe => bad_drop.fe} (89%) rename fec/tests/own/{own-bad-loop.fe => bad_loop.fe} (83%) rename fec/tests/own/{own-bad-move.fe => bad_move.fe} (85%) rename fec/tests/own/{own-bad-proj.fe => bad_proj.fe} (80%) rename fec/tests/own/{ok-defer.fe => ok_defer.fe} (81%) rename fec/tests/own/{ok-owned.fe => ok_owned.fe} (91%) rename fec/tests/pending-backend/{bounds-nocheck.fe => bounds_nocheck.fe} (100%) rename fec/tests/pending-backend/{bounds-trap.fe => bounds_trap.fe} (100%) rename fec/tests/pending-backend/{ownership-drop.fe => ownership_drop.fe} (100%) rename fec/tests/pending-backend/{slice-bounds-trap.fe => slice_bounds_trap.fe} (100%) rename fec/tests/types/{bad-ari.fe => bad_ari.fe} (85%) rename fec/tests/types/{bad-asgn.fe => bad_asgn.fe} (82%) rename fec/tests/types/{bad-cast.fe => bad_cast.fe} (100%) rename fec/tests/types/{bad-cond.fe => bad_cond.fe} (74%) rename fec/tests/types/{bad-mlet.fe => bad_mlet.fe} (79%) rename fec/tests/types/{bad-ret.fe => bad_ret.fe} (69%) rename fec/tests/types/{bad-shwr.fe => bad_shwr.fe} (62%) rename fec/tests/types/{bad-type.fe => bad_type.fe} (86%) rename fec/tests/types/{bad-unit.fe => bad_unit.fe} (77%) rename fec/tests/types/{bad-unk.fe => bad_unk.fe} (72%) rename fec/tests/types/{bad-void.fe => bad_void.fe} (100%) rename fec/tests/types/{ok-arrayctx.fe => ok_arra1.fe} (87%) rename fec/tests/types/{ok-array.fe => ok_array.fe} (92%) rename fec/tests/types/{ok-castwhil.fe => ok_castw.fe} (90%) rename fec/tests/types/{ok-char.fe => ok_char.fe} (92%) rename fec/tests/types/{ok-enum.fe => ok_enum.fe} (96%) rename fec/tests/types/{ok-for.fe => ok_for.fe} (97%) rename fec/tests/types/{ok-hello.fe => ok_hello.fe} (95%) rename fec/tests/types/{ok-mutable.fe => ok_mutab.fe} (92%) rename fec/tests/types/{ok-nested.fe => ok_neste.fe} (92%) rename fec/tests/types/{ok-scopes.fe => ok_scope.fe} (94%) rename fec/tests/types/{ok-str.fe => ok_str.fe} (90%) rename fec/tests/types/{ok-struct.fe => ok_struc.fe} (96%) diff --git a/fec/src/driver.c b/fec/src/driver.c index 7a5c55f..93002fa 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -1,5 +1,6 @@ #include "parser.h" #include "check.h" +#include "resolve.h" #include #include #include @@ -80,6 +81,15 @@ int main(int argc, char **argv) free(src); return d.errors?1:0; } + /* Unit identity before semantic analysis: a unit that is not named + correctly, or does not sit where its name says, cannot be resolved from + another unit either. */ + fe_resolve_unit_identity(&ast,&d,file); + if(d.errors){ + fe_ast_destroy(&ast); + free(src); + return 1; + } fe_check_init(&check,&ast,&d,pointer_bits,no_checks); if(!fe_check_program(&check)){ fe_ast_destroy(&ast); diff --git a/fec/src/parser.c b/fec/src/parser.c index 37b6f89..7d8a8e6 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -302,12 +302,46 @@ static FeNode *statement(FeParser *p) e=expr(p,0); if(is(p,FE_TOK_EQ)||is(p,FE_TOK_PLUS_EQ)||is(p,FE_TOK_MINUS_EQ)||is(p,FE_TOK_STAR_EQ)||is(p,FE_TOK_SLASH_EQ)||is(p,FE_TOK_PERCENT_EQ)||is(p,FE_TOK_AND_EQ)||is(p,FE_TOK_OR_EQ)||is(p,FE_TOK_XOR_EQ)||is(p,FE_TOK_SHL_EQ)||is(p,FE_TOK_SHR_EQ)){n=toknode(p,FE_N_ASSIGN,p->current);n->a=e;next(p);n->b=expr(p,0);}else{n=toknode(p,FE_N_EXPR_STMT,t);n->a=e;}want(p,FE_TOK_SEMI,"expected ';' after statement");return n; } +/* A unit path is dotted: `game.world.map`. It is stored canonically, dots and + all, because that spelling is the unit's identity everywhere else. */ +static char *unit_path(FeParser *p) +{ + char buf[256]; + unsigned long len=0; + if(!is_name(p)) return 0; + for(;;) { + unsigned long n=p->current.length; + if(len && len+1=sizeof buf){error(p,"unit path is too long");return 0;} + memcpy(buf+len,p->current.begin,n); + len+=n; + next(p); + if(!eat(p,FE_TOK_DOT)) break; + if(!is_name(p)){error(p,"expected a name after '.' in unit path");return 0;} + } + return fe_arena_strdup(&p->ast->arena,buf,len); +} + FeNode *fe_parse_unit(FeParser *p) { - FeToken t=p->current, name; FeNode *root; + FeToken t=p->current; FeNode *root; char *path; if(!eat(p,FE_TOK_UNIT)){error(p,"source must start with 'unit'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"unit",4);} - root=toknode(p,FE_N_UNIT,t);if(is_name(p)){name=p->current;root->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length);next(p);}else error(p,"expected unit name");want(p,FE_TOK_SEMI,"expected ';' after unit name"); - while(eat(p,FE_TOK_IMPORT)){FeToken it=p->previous;FeNode *i=toknode(p,FE_N_IMPORT,it);if(is_name(p)){next(p);i->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected import name");want(p,FE_TOK_SEMI,"expected ';' after import");fe_node_add(root,i);} + root=toknode(p,FE_N_UNIT,t); + path=unit_path(p); + if(path) root->text=path; else error(p,"expected unit name"); + want(p,FE_TOK_SEMI,"expected ';' after unit name"); + while(eat(p,FE_TOK_IMPORT)){ + FeToken it=p->previous;FeNode *i=toknode(p,FE_N_IMPORT,it); + path=unit_path(p); + if(path) i->text=path; else error(p,"expected import name"); + /* `as` renames the binding; without it the binding is the last segment. */ + if(eat(p,FE_TOK_AS)) { + if(is_name(p)){i->aux_text=fe_arena_strdup(&p->ast->arena,p->current.begin,p->current.length);next(p);} + else error(p,"expected an alias name after 'as'"); + } + want(p,FE_TOK_SEMI,"expected ';' after import"); + fe_node_add(root,i); + } while(!is(p,FE_TOK_EOF)){FeNode *d=decl(p);if(d)fe_node_add(root,d);} return root; } diff --git a/fec/src/resolve.c b/fec/src/resolve.c new file mode 100644 index 0000000..2cc2aae --- /dev/null +++ b/fec/src/resolve.c @@ -0,0 +1,86 @@ +#include "resolve.h" + +#include + +static int segment_ok(const char *s, unsigned long n, const char **why) +{ + unsigned long i; + if (!n) { *why = "unit path segment is empty"; return 0; } + if (n > FE_UNIT_SEGMENT_MAX) { + *why = "unit path segment is longer than eight characters"; + return 0; + } + if (s[0] < 'a' || s[0] > 'z') { + *why = "unit path segment must start with a lowercase letter"; + return 0; + } + for (i = 1; i < n; ++i) { + char c = s[i]; + if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_') continue; + *why = "unit path segment may only contain lowercase letters, digits and '_'"; + return 0; + } + return 1; +} + +/* Compare a dotted unit path against the source path it was read from. + `game.world.map` matches `.../game/world/map.fe` and nothing else. Only the + trailing segments are compared, since the leading part is the import root. */ +static int path_matches(const char *unit, const char *source) +{ + unsigned long ulen = strlen(unit), slen = strlen(source); + unsigned long u, s; + if (slen < 3 || strcmp(source + slen - 3, ".fe") != 0) return 0; + slen -= 3; + u = ulen; + s = slen; + while (u > 0) { + char uc, sc; + --u; + if (s == 0) return 0; + --s; + uc = unit[u]; + sc = source[s]; + if (uc == '.') { + if (sc != '/' && sc != '\\') return 0; + continue; + } + /* Host filesystems may be case-insensitive; the unit name is the + authority and is lowercase by 8.1, so fold the path side down. */ + if (sc >= 'A' && sc <= 'Z') sc = (char)(sc - 'A' + 'a'); + if (uc != sc) return 0; + } + /* What remains of the source path is the import root, and must end there. */ + return s == 0 || source[s - 1] == '/' || source[s - 1] == '\\'; +} + +int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path) +{ + FeNode *root = ast ? ast->root : 0; + const char *name, *why; + const char *seg; + unsigned long i, len; + int ok = 1; + + if (!root || root->kind != FE_N_UNIT || !root->text) return 0; + name = root->text; + len = strlen(name); + + seg = name; + for (i = 0; i <= len; ++i) { + if (i != len && name[i] != '.') continue; + if (!segment_ok(seg, (unsigned long)(name + i - seg), &why)) { + fe_diag_error(diags, root->loc, why); + ok = 0; + } + seg = name + i + 1; + } + + if (ok && source_path && !path_matches(name, source_path)) { + fe_diag_errorf(diags, root->loc, + "unit %s must be declared in a source file matching its path", + name); + ok = 0; + } + return ok; +} diff --git a/fec/src/resolve.h b/fec/src/resolve.h new file mode 100644 index 0000000..6b49e3d --- /dev/null +++ b/fec/src/resolve.h @@ -0,0 +1,24 @@ +#ifndef FE_RESOLVE_H +#define FE_RESOLVE_H + +#include "ast.h" +#include "diag.h" + +/* Unit-level resolution: identity, import bindings, and the unit graph. + This runs between parsing and semantic checking. It answers questions that + need more than one file -- what a unit is called, what it imports, and + whether those imports exist and terminate -- so that check.c can keep + looking at one function at a time. */ + +/* SPEC 8.1: each segment is ASCII lowercase, starts with a letter, continues + with letters, digits or '_', and is at most eight characters. The limit is + what makes a unit path map to a FAT/DOS 8.3 source path unambiguously. */ +#define FE_UNIT_SEGMENT_MAX 8 + +/* Validate the `unit` declaration against SPEC 8.1, and against the file it was + read from: the path must match the dotted name, so `game.world.map` has to + come from `game/world/map.fe`. `source_path` may be null to skip that half. + Returns non-zero when the unit is well formed. */ +int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path); + +#endif diff --git a/fec/tests/format/bad-ari.fe b/fec/tests/format/bad_ari.fe similarity index 75% rename from fec/tests/format/bad-ari.fe rename to fec/tests/format/bad_ari.fe index 3b29ff8..7dc30b8 100644 --- a/fec/tests/format/bad-ari.fe +++ b/fec/tests/format/bad_ari.fe @@ -1,4 +1,4 @@ -unit m4_bad_arity; +unit bad_ari; fn main() -> i32 { @print("{} {}", 1); diff --git a/fec/tests/format/bad-bufw.fe b/fec/tests/format/bad_bufw.fe similarity index 82% rename from fec/tests/format/bad-bufw.fe rename to fec/tests/format/bad_bufw.fe index 791bd7d..37bf521 100644 --- a/fec/tests/format/bad-bufw.fe +++ b/fec/tests/format/bad_bufw.fe @@ -1,4 +1,4 @@ -unit m4_bad_buffer_writer; +unit bad_bufw; fn main() -> void { var raw: [4]u8 = [0, 0, 0, 0]; diff --git a/fec/tests/format/bad-cls.fe b/fec/tests/format/bad_cls.fe similarity index 76% rename from fec/tests/format/bad-cls.fe rename to fec/tests/format/bad_cls.fe index a321e1c..1f8a720 100644 --- a/fec/tests/format/bad-cls.fe +++ b/fec/tests/format/bad_cls.fe @@ -1,4 +1,4 @@ -unit m4_bad_cls; +unit bad_cls; fn main() -> i32 { @print("}", 1); diff --git a/fec/tests/format/bad-many.fe b/fec/tests/format/bad_many.fe similarity index 76% rename from fec/tests/format/bad-many.fe rename to fec/tests/format/bad_many.fe index 66a7833..ab25401 100644 --- a/fec/tests/format/bad-many.fe +++ b/fec/tests/format/bad_many.fe @@ -1,4 +1,4 @@ -unit m4_bad_many; +unit bad_many; fn main() -> i32 { @print("{}", 1, 2); diff --git a/fec/tests/format/bad-open.fe b/fec/tests/format/bad_open.fe similarity index 75% rename from fec/tests/format/bad-open.fe rename to fec/tests/format/bad_open.fe index 3f1f584..98ee2c8 100644 --- a/fec/tests/format/bad-open.fe +++ b/fec/tests/format/bad_open.fe @@ -1,4 +1,4 @@ -unit m4_bad_open; +unit bad_open; fn main() -> i32 { @print("{", 1); diff --git a/fec/tests/format/bad-run.fe b/fec/tests/format/bad_run.fe similarity index 79% rename from fec/tests/format/bad-run.fe rename to fec/tests/format/bad_run.fe index fa6d7f1..c7945e1 100644 --- a/fec/tests/format/bad-run.fe +++ b/fec/tests/format/bad_run.fe @@ -1,4 +1,4 @@ -unit m4_bad_runtime; +unit bad_run; fn main() -> i32 { var fmt: str = "{}"; diff --git a/fec/tests/format/bad-try.fe b/fec/tests/format/bad_try.fe similarity index 77% rename from fec/tests/format/bad-try.fe rename to fec/tests/format/bad_try.fe index 2bd0e28..0c5da62 100644 --- a/fec/tests/format/bad-try.fe +++ b/fec/tests/format/bad_try.fe @@ -1,4 +1,4 @@ -unit m4_bad_try; +unit bad_try; fn main() -> i32 { try @print("nope"); diff --git a/fec/tests/format/bad-type.fe b/fec/tests/format/bad_type.fe similarity index 86% rename from fec/tests/format/bad-type.fe rename to fec/tests/format/bad_type.fe index e88b48f..9279e47 100644 --- a/fec/tests/format/bad-type.fe +++ b/fec/tests/format/bad_type.fe @@ -1,4 +1,4 @@ -unit m4_bad_type; +unit bad_type; struct Point { x: i32, } diff --git a/fec/tests/format/bad-verb.fe b/fec/tests/format/bad_verb.fe similarity index 76% rename from fec/tests/format/bad-verb.fe rename to fec/tests/format/bad_verb.fe index ea19d9a..9a928ce 100644 --- a/fec/tests/format/bad-verb.fe +++ b/fec/tests/format/bad_verb.fe @@ -1,4 +1,4 @@ -unit m4_bad_verb; +unit bad_verb; fn main() -> i32 { @print("{q}", 1); diff --git a/fec/tests/format/bad-writ.fe b/fec/tests/format/bad_writ.fe similarity index 79% rename from fec/tests/format/bad-writ.fe rename to fec/tests/format/bad_writ.fe index 794ed64..18e33a6 100644 --- a/fec/tests/format/bad-writ.fe +++ b/fec/tests/format/bad_writ.fe @@ -1,4 +1,4 @@ -unit m4_bad_writer; +unit bad_writ; fn main() -> i32 { var x: i32 = 0; diff --git a/fec/tests/format/ok-format.fe b/fec/tests/format/ok_forma.fe similarity index 98% rename from fec/tests/format/ok-format.fe rename to fec/tests/format/ok_forma.fe index 2445625..a830b93 100644 --- a/fec/tests/format/ok-format.fe +++ b/fec/tests/format/ok_forma.fe @@ -1,4 +1,4 @@ -unit m4_format; +unit ok_forma; const FMT: str = "n={} hex={x} c={c} s={s} b={b} {{ok}}\n"; diff --git a/fec/tests/format/ok-prop.fe b/fec/tests/format/ok_prop.fe similarity index 84% rename from fec/tests/format/ok-prop.fe rename to fec/tests/format/ok_prop.fe index 878a77a..49a0051 100644 --- a/fec/tests/format/ok-prop.fe +++ b/fec/tests/format/ok_prop.fe @@ -1,4 +1,4 @@ -unit m4_prop; +unit ok_prop; pub fn propagate(w: io.Writer) -> !void { try @fprint(w, "a{}b", 1); diff --git a/fec/tests/format/ok-try-fpr.fe b/fec/tests/format/ok_try_f.fe similarity index 88% rename from fec/tests/format/ok-try-fpr.fe rename to fec/tests/format/ok_try_f.fe index 94736cc..5429146 100644 --- a/fec/tests/format/ok-try-fpr.fe +++ b/fec/tests/format/ok_try_f.fe @@ -1,4 +1,4 @@ -unit m4_try_fprint; +unit ok_try_f; fn main() -> !void { var raw: [4]u8 = [0, 0, 0, 0]; diff --git a/fec/tests/own/own-bad-clos.fe b/fec/tests/own/bad_clos.fe similarity index 89% rename from fec/tests/own/own-bad-clos.fe rename to fec/tests/own/bad_clos.fe index 530696b..28ff3e4 100644 --- a/fec/tests/own/own-bad-clos.fe +++ b/fec/tests/own/bad_clos.fe @@ -1,4 +1,4 @@ -unit m5_bad_consuming_close; +unit bad_clos; struct FileLike { handle: i32, diff --git a/fec/tests/own/own-bad-cond.fe b/fec/tests/own/bad_cond.fe similarity index 83% rename from fec/tests/own/own-bad-cond.fe rename to fec/tests/own/bad_cond.fe index 90582bc..f46cfe4 100644 --- a/fec/tests/own/own-bad-cond.fe +++ b/fec/tests/own/bad_cond.fe @@ -1,4 +1,4 @@ -unit m5_bad_conditional; +unit bad_cond; fn take(p: ^i32) -> void { mem.destroy(p); } diff --git a/fec/tests/own/own-bad-dbl.fe b/fec/tests/own/bad_dbl.fe similarity index 77% rename from fec/tests/own/own-bad-dbl.fe rename to fec/tests/own/bad_dbl.fe index 32f4b2b..a990673 100644 --- a/fec/tests/own/own-bad-dbl.fe +++ b/fec/tests/own/bad_dbl.fe @@ -1,4 +1,4 @@ -unit m5_bad_double; +unit bad_dbl; fn bad(p: ^i32) -> void { mem.destroy(p); diff --git a/fec/tests/own/own-bad-dest.fe b/fec/tests/own/bad_dest.fe similarity index 69% rename from fec/tests/own/own-bad-dest.fe rename to fec/tests/own/bad_dest.fe index bc60537..8cedd19 100644 --- a/fec/tests/own/own-bad-dest.fe +++ b/fec/tests/own/bad_dest.fe @@ -1,4 +1,4 @@ -unit m5_bad_destroy; +unit bad_dest; fn bad(x: i32) -> void { mem.destroy(x); diff --git a/fec/tests/own/own-bad-drop.fe b/fec/tests/own/bad_drop.fe similarity index 89% rename from fec/tests/own/own-bad-drop.fe rename to fec/tests/own/bad_drop.fe index 7587db6..ad9e9cc 100644 --- a/fec/tests/own/own-bad-drop.fe +++ b/fec/tests/own/bad_drop.fe @@ -1,4 +1,4 @@ -unit m5_bad_drop; +unit bad_drop; struct Box { value: i32, diff --git a/fec/tests/own/own-bad-loop.fe b/fec/tests/own/bad_loop.fe similarity index 83% rename from fec/tests/own/own-bad-loop.fe rename to fec/tests/own/bad_loop.fe index 091d253..c7fd822 100644 --- a/fec/tests/own/own-bad-loop.fe +++ b/fec/tests/own/bad_loop.fe @@ -1,4 +1,4 @@ -unit m5_bad_loop_move; +unit bad_loop; fn take(p: ^i32) -> void { mem.destroy(p); } diff --git a/fec/tests/own/own-bad-move.fe b/fec/tests/own/bad_move.fe similarity index 85% rename from fec/tests/own/own-bad-move.fe rename to fec/tests/own/bad_move.fe index 6614cff..15d25fb 100644 --- a/fec/tests/own/own-bad-move.fe +++ b/fec/tests/own/bad_move.fe @@ -1,4 +1,4 @@ -unit m5_bad_move; +unit bad_move; fn take(p: ^i32) -> void { mem.destroy(p); } diff --git a/fec/tests/own/own-bad-proj.fe b/fec/tests/own/bad_proj.fe similarity index 80% rename from fec/tests/own/own-bad-proj.fe rename to fec/tests/own/bad_proj.fe index bf928ea..0c50987 100644 --- a/fec/tests/own/own-bad-proj.fe +++ b/fec/tests/own/bad_proj.fe @@ -1,4 +1,4 @@ -unit m5_bad_projection_move; +unit bad_proj; struct Holder { p: ^i32 } fn take(p: ^i32) -> void { mem.destroy(p); } diff --git a/fec/tests/own/ok-defer.fe b/fec/tests/own/ok_defer.fe similarity index 81% rename from fec/tests/own/ok-defer.fe rename to fec/tests/own/ok_defer.fe index b21cbe3..e4e2fef 100644 --- a/fec/tests/own/ok-defer.fe +++ b/fec/tests/own/ok_defer.fe @@ -1,4 +1,4 @@ -unit m5_defer; +unit ok_defer; pub fn cleanup(p: ^i32) -> void { defer { mem.destroy(p); } diff --git a/fec/tests/own/ok-owned.fe b/fec/tests/own/ok_owned.fe similarity index 91% rename from fec/tests/own/ok-owned.fe rename to fec/tests/own/ok_owned.fe index 3903ba0..2881628 100644 --- a/fec/tests/own/ok-owned.fe +++ b/fec/tests/own/ok_owned.fe @@ -1,4 +1,4 @@ -unit m5_owned; +unit ok_owned; fn main() -> !void { var p: ^i32 = try mem.create(0); diff --git a/fec/tests/parse/keybuilt.fe b/fec/tests/parse/keybuilt.fe index 21b231d..9f2eba6 100644 --- a/fec/tests/parse/keybuilt.fe +++ b/fec/tests/parse/keybuilt.fe @@ -1,4 +1,4 @@ -unit keywords_and_builtins; +unit keybuilt; pub fn demo() { let a = true and not false; diff --git a/fec/tests/parse/logical.fe b/fec/tests/parse/logical.fe index a38bcc6..8137071 100644 --- a/fec/tests/parse/logical.fe +++ b/fec/tests/parse/logical.fe @@ -1,3 +1,3 @@ // ERROR:logical operator -unit old_logic; +unit logical; fn main() { let x = true && false; } diff --git a/fec/tests/parse/misssemi.fe b/fec/tests/parse/misssemi.fe index c767019..a43b432 100644 --- a/fec/tests/parse/misssemi.fe +++ b/fec/tests/parse/misssemi.fe @@ -1,3 +1,3 @@ // ERROR:expected ';' -unit broken; +unit misssemi; fn main() { let x: i32 = 1 } diff --git a/fec/tests/parse/unclcomm.fe b/fec/tests/parse/unclcomm.fe index fe95c5a..7cab161 100644 --- a/fec/tests/parse/unclcomm.fe +++ b/fec/tests/parse/unclcomm.fe @@ -1,3 +1,3 @@ // ERROR:unterminated block comment -unit broken; +unit unclcomm; /* no ending delimiter diff --git a/fec/tests/parse/v012form.fe b/fec/tests/parse/v012form.fe index db3d4b5..406a63d 100644 --- a/fec/tests/parse/v012form.fe +++ b/fec/tests/parse/v012form.fe @@ -1,4 +1,4 @@ -unit v012_forms; +unit v012form; shared atomic var ticks: u16 = 0; packed struct Packet { diff --git a/fec/tests/pending-backend/bounds-nocheck.fe b/fec/tests/pending-backend/bounds_nocheck.fe similarity index 100% rename from fec/tests/pending-backend/bounds-nocheck.fe rename to fec/tests/pending-backend/bounds_nocheck.fe diff --git a/fec/tests/pending-backend/bounds-trap.fe b/fec/tests/pending-backend/bounds_trap.fe similarity index 100% rename from fec/tests/pending-backend/bounds-trap.fe rename to fec/tests/pending-backend/bounds_trap.fe diff --git a/fec/tests/pending-backend/ownership-drop.fe b/fec/tests/pending-backend/ownership_drop.fe similarity index 100% rename from fec/tests/pending-backend/ownership-drop.fe rename to fec/tests/pending-backend/ownership_drop.fe diff --git a/fec/tests/pending-backend/slice-bounds-trap.fe b/fec/tests/pending-backend/slice_bounds_trap.fe similarity index 100% rename from fec/tests/pending-backend/slice-bounds-trap.fe rename to fec/tests/pending-backend/slice_bounds_trap.fe diff --git a/fec/tests/types/bad-ari.fe b/fec/tests/types/bad_ari.fe similarity index 85% rename from fec/tests/types/bad-ari.fe rename to fec/tests/types/bad_ari.fe index 2ba4b2a..bdf4619 100644 --- a/fec/tests/types/bad-ari.fe +++ b/fec/tests/types/bad_ari.fe @@ -1,4 +1,4 @@ -unit bad_arity; +unit bad_ari; fn add(a: i32, b: i32) -> i32 { return a + b; diff --git a/fec/tests/types/bad-asgn.fe b/fec/tests/types/bad_asgn.fe similarity index 82% rename from fec/tests/types/bad-asgn.fe rename to fec/tests/types/bad_asgn.fe index fac907a..3e921bc 100644 --- a/fec/tests/types/bad-asgn.fe +++ b/fec/tests/types/bad_asgn.fe @@ -1,4 +1,4 @@ -unit bad_assign; +unit bad_asgn; fn main() -> i32 { let value: i32 = 1; diff --git a/fec/tests/types/bad-cast.fe b/fec/tests/types/bad_cast.fe similarity index 100% rename from fec/tests/types/bad-cast.fe rename to fec/tests/types/bad_cast.fe diff --git a/fec/tests/types/bad-cond.fe b/fec/tests/types/bad_cond.fe similarity index 74% rename from fec/tests/types/bad-cond.fe rename to fec/tests/types/bad_cond.fe index 2e706fa..a3968f1 100644 --- a/fec/tests/types/bad-cond.fe +++ b/fec/tests/types/bad_cond.fe @@ -1,4 +1,4 @@ -unit bad_condition; +unit bad_cond; fn main() -> i32 { if 1 { return 0; } diff --git a/fec/tests/types/bad-mlet.fe b/fec/tests/types/bad_mlet.fe similarity index 79% rename from fec/tests/types/bad-mlet.fe rename to fec/tests/types/bad_mlet.fe index af257c1..a3960b0 100644 --- a/fec/tests/types/bad-mlet.fe +++ b/fec/tests/types/bad_mlet.fe @@ -1,4 +1,4 @@ -unit m3_bad_mut_let; +unit bad_mlet; fn bad() -> void { var raw: [2]u8 = [1, 2]; diff --git a/fec/tests/types/bad-ret.fe b/fec/tests/types/bad_ret.fe similarity index 69% rename from fec/tests/types/bad-ret.fe rename to fec/tests/types/bad_ret.fe index 914cd99..a151ab3 100644 --- a/fec/tests/types/bad-ret.fe +++ b/fec/tests/types/bad_ret.fe @@ -1,4 +1,4 @@ -unit bad_return; +unit bad_ret; fn main() -> i32 { return true; diff --git a/fec/tests/types/bad-shwr.fe b/fec/tests/types/bad_shwr.fe similarity index 62% rename from fec/tests/types/bad-shwr.fe rename to fec/tests/types/bad_shwr.fe index aa4f105..7c7881f 100644 --- a/fec/tests/types/bad-shwr.fe +++ b/fec/tests/types/bad_shwr.fe @@ -1,4 +1,4 @@ -unit m3_bad_shared_write; +unit bad_shwr; fn bad(s: []u8) -> void { s[0] = 1; diff --git a/fec/tests/types/bad-type.fe b/fec/tests/types/bad_type.fe similarity index 86% rename from fec/tests/types/bad-type.fe rename to fec/tests/types/bad_type.fe index 1f277fe..15a4aca 100644 --- a/fec/tests/types/bad-type.fe +++ b/fec/tests/types/bad_type.fe @@ -1,4 +1,4 @@ -unit bad_types; +unit bad_type; fn add(a: i32, b: i32) -> i32 { return a + b; diff --git a/fec/tests/types/bad-unit.fe b/fec/tests/types/bad_unit.fe similarity index 77% rename from fec/tests/types/bad-unit.fe rename to fec/tests/types/bad_unit.fe index 51f6092..99d8bbd 100644 --- a/fec/tests/types/bad-unit.fe +++ b/fec/tests/types/bad_unit.fe @@ -1,4 +1,4 @@ -unit bad_uninit; +unit bad_unit; fn main() -> i32 { var value: i32; diff --git a/fec/tests/types/bad-unk.fe b/fec/tests/types/bad_unk.fe similarity index 72% rename from fec/tests/types/bad-unk.fe rename to fec/tests/types/bad_unk.fe index 51d1b01..b868c3b 100644 --- a/fec/tests/types/bad-unk.fe +++ b/fec/tests/types/bad_unk.fe @@ -1,4 +1,4 @@ -unit bad_unknown; +unit bad_unk; fn main() -> i32 { return missing_name; diff --git a/fec/tests/types/bad-void.fe b/fec/tests/types/bad_void.fe similarity index 100% rename from fec/tests/types/bad-void.fe rename to fec/tests/types/bad_void.fe diff --git a/fec/tests/types/badarr.fe b/fec/tests/types/badarr.fe index f419f7a..11007cb 100644 --- a/fec/tests/types/badarr.fe +++ b/fec/tests/types/badarr.fe @@ -1,4 +1,4 @@ -unit fail_m3_array; +unit badarr; fn main() -> i32 { let a: [2]i32 = [1, true, 3]; return a[0]; diff --git a/fec/tests/types/badchar.fe b/fec/tests/types/badchar.fe index aed42d4..9d1a1d4 100644 --- a/fec/tests/types/badchar.fe +++ b/fec/tests/types/badchar.fe @@ -1,4 +1,4 @@ -unit fail_m3_char; +unit badchar; fn main() -> i32 { let u: u8 = 'A'; diff --git a/fec/tests/types/badcycle.fe b/fec/tests/types/badcycle.fe index e96c17e..532ea1e 100644 --- a/fec/tests/types/badcycle.fe +++ b/fec/tests/types/badcycle.fe @@ -1,4 +1,4 @@ -unit fail_m3_cycle; +unit badcycle; struct A { b: B, } struct B { a: A, } diff --git a/fec/tests/types/badfield.fe b/fec/tests/types/badfield.fe index 350edbb..4fa664c 100644 --- a/fec/tests/types/badfield.fe +++ b/fec/tests/types/badfield.fe @@ -1,4 +1,4 @@ -unit fail_m3_let_field; +unit badfield; struct Point { x: i32, y: i32, } fn main() -> i32 { diff --git a/fec/tests/types/badfld.fe b/fec/tests/types/badfld.fe index dd9e0c4..68704be 100644 --- a/fec/tests/types/badfld.fe +++ b/fec/tests/types/badfld.fe @@ -1,4 +1,4 @@ -unit fail_m3_fields; +unit badfld; struct Point { x: i32, y: i32, } fn main() -> i32 { let p: Point = Point{ x: 1 }; diff --git a/fec/tests/types/badindex.fe b/fec/tests/types/badindex.fe index 88e8bca..06b1b10 100644 --- a/fec/tests/types/badindex.fe +++ b/fec/tests/types/badindex.fe @@ -1,4 +1,4 @@ -unit fail_m3_let_index; +unit badindex; fn main() -> i32 { let a: [2]i32 = [1, 2]; diff --git a/fec/tests/types/badmat.fe b/fec/tests/types/badmat.fe index f1c1d54..3c7df6a 100644 --- a/fec/tests/types/badmat.fe +++ b/fec/tests/types/badmat.fe @@ -1,4 +1,4 @@ -unit fail_m3_match; +unit badmat; enum Shape { Empty, Circle(i32), } fn main() -> i32 { match Shape.Empty { Empty => 0; } diff --git a/fec/tests/types/badstr.fe b/fec/tests/types/badstr.fe index 2896f0c..6219d9a 100644 --- a/fec/tests/types/badstr.fe +++ b/fec/tests/types/badstr.fe @@ -1,4 +1,4 @@ -unit fail_m3_str; +unit badstr; fn main() -> i32 { var text: str = "abc"; text[0] = 'z'; diff --git a/fec/tests/types/ok-arrayctx.fe b/fec/tests/types/ok_arra1.fe similarity index 87% rename from fec/tests/types/ok-arrayctx.fe rename to fec/tests/types/ok_arra1.fe index 9e6d047..487b1ec 100644 --- a/fec/tests/types/ok-arrayctx.fe +++ b/fec/tests/types/ok_arra1.fe @@ -1,4 +1,4 @@ -unit m3_arrayctx; +unit ok_arra1; fn main() -> i32 { let bytes: [3]u8 = [1, 2, 3]; diff --git a/fec/tests/types/ok-array.fe b/fec/tests/types/ok_array.fe similarity index 92% rename from fec/tests/types/ok-array.fe rename to fec/tests/types/ok_array.fe index a8ff358..bf748bc 100644 --- a/fec/tests/types/ok-array.fe +++ b/fec/tests/types/ok_array.fe @@ -1,4 +1,4 @@ -unit m3_array; +unit ok_array; fn main() -> i32 { let a: [3]i32 = [1, 2, 3]; diff --git a/fec/tests/types/ok-castwhil.fe b/fec/tests/types/ok_castw.fe similarity index 90% rename from fec/tests/types/ok-castwhil.fe rename to fec/tests/types/ok_castw.fe index c54b114..2e962f5 100644 --- a/fec/tests/types/ok-castwhil.fe +++ b/fec/tests/types/ok_castw.fe @@ -1,4 +1,4 @@ -unit cast_while; +unit ok_castw; pub fn main() -> i32 { var x: i16 = 0; diff --git a/fec/tests/types/ok-char.fe b/fec/tests/types/ok_char.fe similarity index 92% rename from fec/tests/types/ok-char.fe rename to fec/tests/types/ok_char.fe index 253ff85..e6939b7 100644 --- a/fec/tests/types/ok-char.fe +++ b/fec/tests/types/ok_char.fe @@ -1,4 +1,4 @@ -unit m3_char; +unit ok_char; fn main() -> i32 { let c: char = '\u0041'; diff --git a/fec/tests/types/ok-enum.fe b/fec/tests/types/ok_enum.fe similarity index 96% rename from fec/tests/types/ok-enum.fe rename to fec/tests/types/ok_enum.fe index 2e2e934..581cbb3 100644 --- a/fec/tests/types/ok-enum.fe +++ b/fec/tests/types/ok_enum.fe @@ -1,4 +1,4 @@ -unit m3_enum; +unit ok_enum; enum Shape { Empty, Circle(i32), Rect { w: i32, h: i32, }, } diff --git a/fec/tests/types/ok-for.fe b/fec/tests/types/ok_for.fe similarity index 97% rename from fec/tests/types/ok-for.fe rename to fec/tests/types/ok_for.fe index e591000..3aab7bf 100644 --- a/fec/tests/types/ok-for.fe +++ b/fec/tests/types/ok_for.fe @@ -1,4 +1,4 @@ -unit m3_for; +unit ok_for; fn main() -> i32 { var total: i32 = 0; diff --git a/fec/tests/types/ok-hello.fe b/fec/tests/types/ok_hello.fe similarity index 95% rename from fec/tests/types/ok-hello.fe rename to fec/tests/types/ok_hello.fe index aec68c7..e55fedb 100644 --- a/fec/tests/types/ok-hello.fe +++ b/fec/tests/types/ok_hello.fe @@ -1,4 +1,4 @@ -unit hello; +unit ok_hello; fn add(a: i32, b: i32) -> i32 { return a + b; diff --git a/fec/tests/types/ok-mutable.fe b/fec/tests/types/ok_mutab.fe similarity index 92% rename from fec/tests/types/ok-mutable.fe rename to fec/tests/types/ok_mutab.fe index 9c31d51..dc596a2 100644 --- a/fec/tests/types/ok-mutable.fe +++ b/fec/tests/types/ok_mutab.fe @@ -1,4 +1,4 @@ -unit m3_mutable; +unit ok_mutab; fn takes_shared(s: []u8) -> u8 { return s[0]; } diff --git a/fec/tests/types/ok-nested.fe b/fec/tests/types/ok_neste.fe similarity index 92% rename from fec/tests/types/ok-nested.fe rename to fec/tests/types/ok_neste.fe index 3f71cc9..e8c0485 100644 --- a/fec/tests/types/ok-nested.fe +++ b/fec/tests/types/ok_neste.fe @@ -1,4 +1,4 @@ -unit m3_nested; +unit ok_neste; struct Outer { inner: Inner, } struct Inner { value: i32, } diff --git a/fec/tests/types/ok-scopes.fe b/fec/tests/types/ok_scope.fe similarity index 94% rename from fec/tests/types/ok-scopes.fe rename to fec/tests/types/ok_scope.fe index b0d6168..153398f 100644 --- a/fec/tests/types/ok-scopes.fe +++ b/fec/tests/types/ok_scope.fe @@ -1,4 +1,4 @@ -unit scopes; +unit ok_scope; fn register(switch: i32) -> i32 { let auto: i32 = switch; diff --git a/fec/tests/types/ok-str.fe b/fec/tests/types/ok_str.fe similarity index 90% rename from fec/tests/types/ok-str.fe rename to fec/tests/types/ok_str.fe index f0077fd..fec3064 100644 --- a/fec/tests/types/ok-str.fe +++ b/fec/tests/types/ok_str.fe @@ -1,4 +1,4 @@ -unit m3_str; +unit ok_str; fn main() -> i32 { let text: str = "abc"; diff --git a/fec/tests/types/ok-struct.fe b/fec/tests/types/ok_struc.fe similarity index 96% rename from fec/tests/types/ok-struct.fe rename to fec/tests/types/ok_struc.fe index 2d7e2b1..1754eb9 100644 --- a/fec/tests/types/ok-struct.fe +++ b/fec/tests/types/ok_struc.fe @@ -1,4 +1,4 @@ -unit m3_struct; +unit ok_struc; struct Point { x: i32, y: i32, } packed struct PackedPoint { x: u8, y: i32, } diff --git a/tests/run.py b/tests/run.py index 20700dd..ff66496 100644 --- a/tests/run.py +++ b/tests/run.py @@ -34,7 +34,7 @@ ROOT = Path(__file__).resolve().parent.parent FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", - "check", "driver") + "check", "resolve", "driver") # Fixtures live here until there is a code generator to run them against. QUARANTINE = "pending-backend" From 38dddc234c5c2dd5fc85625772f5871c3a320bc8 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 03:54:43 +0900 Subject: [PATCH 114/184] implement: load the unit graph -- imports, cycles, bindings The driver handled one file. It now loads the graph rooted at the entry file and checks every unit in it. The import root is derived rather than configured: a unit named `a.b` read from `/a/b.fe` fixes ``, so a sibling `import c.d;` is looked for at `/c/d.fe`. That is enough for the fixtures and for any tree that follows 8.1, and it means there is no path flag to get wrong yet. Loading is depth-first with the chain of units currently open kept on a stack, so meeting one again is a cycle rather than a revisit -- a unit reached twice by different paths is loaded once. Cycles, imports with no source file, and two imports claiming the same binding are all reported. One thing this exposed: FeDiags held a single source buffer, so once a build spanned several files every excerpt was drawn from whichever file was parsed last. `cycle/b.fe:3` printed the text of a.fe. fe_diags_source switches it, and loading and checking both set it per unit. The cycle fixtures lost their line markers on purpose. Which import closes the cycle depends on which file you enter from -- entering at a.fe reports b.fe, entering at b.fe reports a.fe -- so pinning a line would pin an arbitrary half of a symmetric pair. The message is pinned; the line is not. units: missing, bindconf and cycle pass, on top of the identity cases. 146 -> 148 of 188. What is left in units/ needs cross-unit name resolution and visibility: an importer still cannot see `util.answer`. --- fec/src/diag.c | 6 + fec/src/diag.h | 4 + fec/src/driver.c | 40 +++---- fec/src/resolve.c | 227 +++++++++++++++++++++++++++++++++++++ fec/src/resolve.h | 35 ++++++ fec/tests/units/cycle/a.fe | 1 + fec/tests/units/cycle/b.fe | 2 +- 7 files changed, 295 insertions(+), 20 deletions(-) diff --git a/fec/src/diag.c b/fec/src/diag.c index d0df0f4..53919a3 100644 --- a/fec/src/diag.c +++ b/fec/src/diag.c @@ -61,6 +61,12 @@ void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len) d->source_len=source_len; } +void fe_diags_source(FeDiags *d, const char *source, unsigned long source_len) +{ + d->source=source; + d->source_len=source_len; +} + void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg) { d->errors++; diff --git a/fec/src/diag.h b/fec/src/diag.h index df27869..b47f53b 100644 --- a/fec/src/diag.h +++ b/fec/src/diag.h @@ -24,6 +24,10 @@ typedef struct FeDiags { FILE *fe_diag_stream(void); void fe_diags_init(FeDiags *d, const char *source, unsigned long source_len); + +/* Point the excerpt printer at a different file. A build spans several + units, and an excerpt drawn from the wrong buffer is worse than none. */ +void fe_diags_source(FeDiags *d, const char *source, unsigned long source_len); void fe_diag_error(FeDiags *d, FeLoc loc, const char *msg); void fe_diag_errorf(FeDiags *d, FeLoc loc, const char *msg, const char *arg); void fe_diag_note(FeLoc loc, const char *msg); diff --git a/fec/src/driver.c b/fec/src/driver.c index 93002fa..168380b 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -81,25 +81,27 @@ int main(int argc, char **argv) free(src); return d.errors?1:0; } - /* Unit identity before semantic analysis: a unit that is not named - correctly, or does not sit where its name says, cannot be resolved from - another unit either. */ - fe_resolve_unit_identity(&ast,&d,file); - if(d.errors){ - fe_ast_destroy(&ast); - free(src); - return 1; - } - fe_check_init(&check,&ast,&d,pointer_bits,no_checks); - if(!fe_check_program(&check)){ - fe_ast_destroy(&ast); - free(src); - return 1; - } - /* Semantic analysis is the last pass there is. A code generator attaches - here; until then --check and the default path are the same thing. */ - (void)check_only; fe_ast_destroy(&ast); free(src); - return d.errors?1:0; + src=0; + /* Load the whole unit graph rooted at this file: identity, imports, + cycles and bindings. The entry file is parsed a second time as part of + it, which costs one file read and keeps the graph the single owner of + every unit's AST. */ + { + FeBuild build; + unsigned u; + int ok=fe_build_load(&build,file,&d); + for(u=0;ok && usource,unit->size); + fe_check_init(&check,&unit->ast,&d,pointer_bits,no_checks); + if(!fe_check_program(&check)) ok=0; + } + fe_build_destroy(&build); + /* Semantic analysis is the last pass there is. A code generator + attaches here; until then --check and the default path agree. */ + (void)check_only; + return (!ok||d.errors)?1:0; + } } diff --git a/fec/src/resolve.c b/fec/src/resolve.c index 2cc2aae..5de4c13 100644 --- a/fec/src/resolve.c +++ b/fec/src/resolve.c @@ -1,5 +1,8 @@ #include "resolve.h" +#include "parser.h" +#include +#include #include static int segment_ok(const char *s, unsigned long n, const char **why) @@ -54,6 +57,78 @@ static int path_matches(const char *unit, const char *source) return s == 0 || source[s - 1] == '/' || source[s - 1] == '\\'; } +static char *read_source(const char *path, unsigned long *size) +{ + FILE *f; + long n; + char *p; + f = fopen(path, "rb"); + if (!f) return 0; + fseek(f, 0, SEEK_END); + n = ftell(f); + fseek(f, 0, SEEK_SET); + if (n < 0) { fclose(f); return 0; } + p = (char *)malloc((unsigned long)n + 1); + if (!p) { fclose(f); return 0; } + if (fread(p, 1, (unsigned long)n, f) != (unsigned long)n) { + fclose(f); free(p); return 0; + } + p[n] = 0; + fclose(f); + *size = (unsigned long)n; + return p; +} + +/* /a/b.fe for the unit a.b */ +static void unit_source_path(char *out, unsigned long cap, + const char *root, const char *unit) +{ + unsigned long i = 0, j = 0; + while (root[i] && j + 1 < cap) out[j++] = root[i++]; + if (j && out[j - 1] != '/' && out[j - 1] != '\\' && j + 1 < cap) out[j++] = '/'; + for (i = 0; unit[i] && j + 1 < cap; ++i) + out[j++] = unit[i] == '.' ? '/' : unit[i]; + if (j + 3 < cap) { out[j++] = '.'; out[j++] = 'f'; out[j++] = 'e'; } + out[j] = 0; +} + +/* Strip the unit's own path from the file it was read from; what is left is + the import root that every other unit is looked up under. */ +static void import_root(char *out, unsigned long cap, + const char *source, const char *unit) +{ + unsigned long slen = strlen(source); + unsigned long dots = 0, i, cut; + for (i = 0; unit[i]; ++i) if (unit[i] == '.') ++dots; + if (slen >= 3) slen -= 3; + cut = slen; + for (i = 0; i <= dots; ++i) { + while (cut > 0 && source[cut - 1] != '/' && source[cut - 1] != '\\') --cut; + if (i < dots && cut > 0) --cut; + } + if (cut >= cap) cut = cap - 1; + memcpy(out, source, cut); + out[cut] = 0; + if (!cut) { out[0] = '.'; out[1] = 0; } +} + +static FeUnit *find_unit(FeBuild *b, const char *name) +{ + unsigned i; + for (i = 0; i < b->count; ++i) + if (strcmp(b->units[i].name, name) == 0) return &b->units[i]; + return 0; +} + +const char *fe_import_binding(const FeNode *import) +{ + const char *dot; + if (!import) return 0; + if (import->aux_text) return import->aux_text; + dot = import->text ? strrchr(import->text, '.') : 0; + return dot ? dot + 1 : import->text; +} + int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path) { FeNode *root = ast ? ast->root : 0; @@ -84,3 +159,155 @@ int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path } return ok; } + +/* Depth-first load. `stack` is the chain of units currently being loaded, so + meeting one again is a cycle rather than a repeat visit. */ +static int load_unit(FeBuild *b, const char *name, FeLoc from, int have_from, + const char **stack, unsigned depth) +{ + FeUnit *unit; + FeNode *n; + FeParser p; + unsigned long size; + unsigned i; + int ok = 1; + + for (i = 0; i < depth; ++i) { + if (strcmp(stack[i], name) == 0) { + fe_diag_errorf(b->diags, from, "import of %s forms a cycle", name); + return 0; + } + } + if (find_unit(b, name)) return 1; + if (b->count >= FE_BUILD_UNIT_MAX) { + fe_diag_error(b->diags, from, "too many units in one build"); + return 0; + } + if (strlen(name) >= FE_UNIT_PATH_MAX) { + fe_diag_errorf(b->diags, from, "unit path is too long: %s", name); + return 0; + } + unit = &b->units[b->count]; + memset(unit, 0, sizeof *unit); + strcpy(unit->name, name); + unit_source_path(unit->path, sizeof unit->path, b->root, name); + unit->source = read_source(unit->path, &size); + if (!unit->source) { + if (have_from) + fe_diag_errorf(b->diags, from, "import %s has no source file", name); + else + fe_diag_errorf(b->diags, from, "cannot open %s", unit->path); + return 0; + } + b->count++; + unit->size = size; + fe_ast_init(&unit->ast); + /* Diagnostics from here on belong to this file. */ + fe_diags_source(b->diags, unit->source, size); + fe_parser_init(&p, &unit->ast, unit->source, size, unit->path, b->diags); + unit->ast.root = fe_parse_unit(&p); + unit->loaded = 1; + if (!fe_resolve_unit_identity(&unit->ast, b->diags, unit->path)) ok = 0; + + stack[depth] = unit->name; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) { + if (n->kind != FE_N_IMPORT || !n->text) continue; + /* The import statement is where the reader has to make a change, so + the diagnostic points there rather than at the unit it names. */ + if (!load_unit(b, n->text, n->loc, 1, stack, depth + 1)) ok = 0; + fe_diags_source(b->diags, unit->source, unit->size); + } + stack[depth] = 0; + return ok; +} + +/* A binding names one unit inside one importer; two imports cannot claim it. */ +static int check_bindings(FeBuild *b, FeUnit *unit) +{ + FeNode *n, *m; + int ok = 1; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) { + const char *a; + if (n->kind != FE_N_IMPORT) continue; + a = fe_import_binding(n); + if (!a) continue; + for (m = unit->ast.root->children; m != n; m = m->next) { + const char *other; + if (m->kind != FE_N_IMPORT) continue; + other = fe_import_binding(m); + if (other && strcmp(a, other) == 0) { + fe_diag_errorf(b->diags, n->loc, + "import binding %s is already taken; use an alias", a); + ok = 0; + } + } + } + return ok; +} + +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags) +{ + const char *stack[FE_BUILD_UNIT_MAX]; + FeAst probe; + FeParser p; + char *source; + char name[FE_UNIT_PATH_MAX]; + FeLoc loc; + unsigned long size; + unsigned i; + int ok; + + memset(build, 0, sizeof *build); + build->diags = diags; + + /* The entry file fixes the import root, so it has to be parsed far enough + to know its own name before anything else can be found. */ + source = read_source(entry, &size); + if (!source) { + FeLoc none; + none.file = entry; none.line = 0; none.col = 0; + fe_diag_errorf(diags, none, "cannot open %s", entry); + return 0; + } + fe_ast_init(&probe); + fe_parser_init(&p, &probe, source, size, entry, diags); + probe.root = fe_parse_unit(&p); + if (!probe.root || !probe.root->text || diags->errors) { + fe_ast_destroy(&probe); + free(source); + return 0; + } + import_root(build->root, sizeof build->root, entry, probe.root->text); + strncpy(name, probe.root->text, sizeof name - 1); + name[sizeof name - 1] = 0; + loc = probe.root->loc; + fe_ast_destroy(&probe); + free(source); + + ok = load_unit(build, name, loc, 0, stack, 0); + for (i = 0; i < build->count; ++i) + if (!check_bindings(build, &build->units[i])) ok = 0; + return ok && diags->errors == 0; +} + +void fe_build_destroy(FeBuild *build) +{ + unsigned i; + for (i = 0; i < build->count; ++i) { + if (build->units[i].loaded) fe_ast_destroy(&build->units[i].ast); + free(build->units[i].source); + } + build->count = 0; +} + +FeUnit *fe_build_binding(FeBuild *build, FeUnit *unit, const char *binding) +{ + FeNode *n; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) { + const char *bound; + if (n->kind != FE_N_IMPORT || !n->text) continue; + bound = fe_import_binding(n); + if (bound && strcmp(bound, binding) == 0) return find_unit(build, n->text); + } + return 0; +} diff --git a/fec/src/resolve.h b/fec/src/resolve.h index 6b49e3d..8116935 100644 --- a/fec/src/resolve.h +++ b/fec/src/resolve.h @@ -14,6 +14,25 @@ with letters, digits or '_', and is at most eight characters. The limit is what makes a unit path map to a FAT/DOS 8.3 source path unambiguously. */ #define FE_UNIT_SEGMENT_MAX 8 +#define FE_UNIT_PATH_MAX 128 +#define FE_BUILD_UNIT_MAX 64 + +typedef struct FeUnit { + char name[FE_UNIT_PATH_MAX]; /* canonical dotted path */ + char path[260]; /* source file it was read from */ + FeAst ast; + char *source; /* owned; freed with the build */ + unsigned long size; + int loaded; + int checked; +} FeUnit; + +typedef struct FeBuild { + FeUnit units[FE_BUILD_UNIT_MAX]; + unsigned count; + char root[260]; /* import root: where unit paths start */ + FeDiags *diags; +} FeBuild; /* Validate the `unit` declaration against SPEC 8.1, and against the file it was read from: the path must match the dotted name, so `game.world.map` has to @@ -21,4 +40,20 @@ Returns non-zero when the unit is well formed. */ int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path); +/* Load `entry` and everything it imports, transitively. + + The import root is derived from the entry file: a unit named `a.b` read from + `/a/b.fe` fixes ``, so a sibling `import c.d;` is looked for at + `/c/d.fe`. Reports missing imports, import cycles, and binding + conflicts. Returns non-zero when the whole graph loaded cleanly. */ +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags); +void fe_build_destroy(FeBuild *build); + +/* The unit a binding refers to inside `unit`, or null. + The binding is the last segment of the import path unless `as` renamed it. */ +FeUnit *fe_build_binding(FeBuild *build, FeUnit *unit, const char *binding); + +/* The local name an import introduces: its alias, or the last path segment. */ +const char *fe_import_binding(const FeNode *import); + #endif diff --git a/fec/tests/units/cycle/a.fe b/fec/tests/units/cycle/a.fe index 03dbe1c..938e94a 100644 --- a/fec/tests/units/cycle/a.fe +++ b/fec/tests/units/cycle/a.fe @@ -1,3 +1,4 @@ +// ERROR:cycle unit a; import b; diff --git a/fec/tests/units/cycle/b.fe b/fec/tests/units/cycle/b.fe index 4ca3232..c6ee482 100644 --- a/fec/tests/units/cycle/b.fe +++ b/fec/tests/units/cycle/b.fe @@ -1,4 +1,4 @@ -// ERROR:3:cycle +// ERROR:cycle unit b; import a; From d1a402008718fc6260328e6969547ad2ce5decd2 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:05:11 +0900 Subject: [PATCH 115/184] =?UTF-8?q?own=20fixture=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=AA=85=EA=B3=BC=20unit=20=EC=8B=9D=EB=B3=84=EC=9E=90=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fec/tests/own/{badgmut.fe => globalm.fe} | 2 +- fec/tests/own/{badlocsl.fe => localesc.fe} | 2 +- fec/tests/own/{badslfld.fe => self_fld.fe} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename fec/tests/own/{badgmut.fe => globalm.fe} (87%) rename fec/tests/own/{badlocsl.fe => localesc.fe} (86%) rename fec/tests/own/{badslfld.fe => self_fld.fe} (78%) diff --git a/fec/tests/own/badgmut.fe b/fec/tests/own/globalm.fe similarity index 87% rename from fec/tests/own/badgmut.fe rename to fec/tests/own/globalm.fe index 38bd2f8..1a2ed8d 100644 --- a/fec/tests/own/badgmut.fe +++ b/fec/tests/own/globalm.fe @@ -1,5 +1,5 @@ // ERROR:7:global -unit badgmut; +unit globalm; var VALUE: i32 = 0; diff --git a/fec/tests/own/badlocsl.fe b/fec/tests/own/localesc.fe similarity index 86% rename from fec/tests/own/badlocsl.fe rename to fec/tests/own/localesc.fe index bdc8eff..1d62f43 100644 --- a/fec/tests/own/badlocsl.fe +++ b/fec/tests/own/localesc.fe @@ -1,5 +1,5 @@ // ERROR:6:reference -unit badlocsl; +unit localesc; fn bad() -> []u8 { let a: [2]u8 = [1 as u8, 2 as u8]; diff --git a/fec/tests/own/badslfld.fe b/fec/tests/own/self_fld.fe similarity index 78% rename from fec/tests/own/badslfld.fe rename to fec/tests/own/self_fld.fe index 63b6c5a..c025b1f 100644 --- a/fec/tests/own/badslfld.fe +++ b/fec/tests/own/self_fld.fe @@ -1,5 +1,5 @@ // ERROR:5:reference -unit badslfld; +unit self_fld; struct Bad { bytes: []u8, From 88b0c83538fbe462f36c1a9e47fa57c7b3639a72 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:05:13 +0900 Subject: [PATCH 116/184] =?UTF-8?q?fixture=20README=EB=A5=BC=20=ED=98=84?= =?UTF-8?q?=EC=9E=AC=20=EC=83=81=ED=83=9C=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fec/tests/generic/README.md | 33 +++++++++++++------------- fec/tests/optional/README.md | 14 +++++++---- fec/tests/own/README.md | 17 ++++++------- fec/tests/units/README.md | 46 ++++++++++++++++++++---------------- 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/fec/tests/generic/README.md b/fec/tests/generic/README.md index 5fec504..b00050e 100644 --- a/fec/tests/generic/README.md +++ b/fec/tests/generic/README.md @@ -1,21 +1,20 @@ -# M9 fixtures +# 제네릭 -M9 adds comptime type parameters and monomorphization. +이 디렉터리는 제네릭 타입/함수/인스턴스화 관련 고정 fixture입니다. -`ok*.fe` must compile; `bad*.fe` must fail according to the first-line marker. -`defscope/` and `okscope/` are multi-unit definition-scope tests and require M8 imports. `okscope/` verifies that an exported generic may use its definition unit's private helper through `.fei` support metadata. +실행 방법: -Coverage: -- generic functions and structs, -- multiple and duplicate instantiations, -- type aliases as comptime type values, -- `T == U` and `@is_int(T)` in comptime, -- instantiation-time operation errors, -- arity/type-argument errors, -- runtime `type` values forbidden, -- definition-unit name lookup, -- recursive instantiation depth limit. -- rejection of user value generics and generic type inference, -- same-instance recursive request reuse and selected-out `comptime if` semantic skipping. +`uv run python tests/run.py -k generic` -M9's DOS gate should additionally inspect generated `fe_generics.c`: `okdedup.fe` must emit one body for the repeated `(id, i32)` instantiation. +현재 수록 케이스: + +- `okalias.fe`, `okbox.fe`, `okdedup.fe`, `okid.fe`, `okisint.fe` +- `okmulti.fe`, `oknested.fe`, `okpair.fe`, `okscope/main.fe`, `okscope/lib.fe` +- `oksamrec.fe`, `okskip.fe`, `oktypeeq.fe` +- `badarity.fe`, `badarg.fe`, `badbody.fe`, `baddepth.fe`, `baddist.fe` +- `badfew.fe`, `badinfer.fe`, `badop.fe`, `badtype.fe`, `badvalue.fe` +- `defscope/main.fe`, `defscope/lib.fe` + +미구현/미완료 구간: + +현재 다수의 제네릭 케이스가 통과하지 않습니다. diff --git a/fec/tests/optional/README.md b/fec/tests/optional/README.md index 237a2e6..d9212a1 100644 --- a/fec/tests/optional/README.md +++ b/fec/tests/optional/README.md @@ -1,8 +1,12 @@ -# M7 fixtures +# 옵션/에러유니온 -M7 adds optionals and error unions on top of the M6 ownership model. +이 디렉터리는 `?T`/`null`/`try`/`catch`/`orelse`/명목 에러 유니온 동작을 검증합니다. -`ok*.fe` must compile. `bad*.fe` must fail according to the first-line error marker. -The files are registered in `src/ferrolang_vm/registry.py`. +실행 방법: -Coverage: contextual `null`, `?T`, `.?`, `Some`/`None` pattern-only non-destructive views, `mem.replace` extraction, lazy `orelse`/`catch`, nominal error unions, `try`, block/short `catch`, and error code/name uniqueness, plus R4/R7 interactions with optional references/owners. +`uv run python tests/run.py -k optional` + +- `ok*.fe`는 통과해야 합니다. +- `bad*.fe`는 실패해야 하며, 첫 줄 `// ERROR:...` 마커가 있으면 그 패턴을 따른다. + +실행은 프런트엔드(`--check`) 기준이며, 코드 생성 단계는 포함하지 않습니다. diff --git a/fec/tests/own/README.md b/fec/tests/own/README.md index 3d0fcd7..b1fcff9 100644 --- a/fec/tests/own/README.md +++ b/fec/tests/own/README.md @@ -1,11 +1,12 @@ -# M6 fixtures +# 소유권과 대여 -These fixtures pin the M6 ownership/borrow rules before implementation. +이 디렉터리는 `fec/tests`에서 소유권(Ownership)과 대여(Borrow) 동작을 검증하는 고정 fixture 입니다. -- `ok*.fe` must compile with `--target=bits32 --emit-c`. -- `bad*.fe` must fail compilation; the first line follows the `// ERROR::` convention from SPEC §12. -- They are registered in `src/ferrolang_vm/registry.py`, which is what decides whether a milestone runs. -- When M6 starts, wire this directory into the DOS/QEMU gate without changing the expected result of any fixture. -- M6 also owns the general-global borrow restriction from R10 because AGENTS.md explicitly groups that change with the `own.c` state-machine work. +실행 방법: -Coverage: R4 storage restrictions, R5 scope, R6 shared/exclusive liveness, root-local field/index conflicts, call-only `&mut -> &` reborrows, branch/loop `MaybeMoved` and initialization-state merging, R7 invalidation, R8 derived-return provenance joins, defer lifetime extension, and global borrow restrictions. +`uv run python tests/run.py -k own` + +- `ok*.fe`는 통과해야 합니다. +- `bad*.fe`는 실패해야 하며, 첫 줄 `// ERROR:...` 마커가 있으면 그 패턴을 따른다. + +현재 구현 범위는 타입 검사/의미 검사를 통과한 소스 기반만 포함하고, 별도 코드 생성/런타임 동작은 포함하지 않습니다. diff --git a/fec/tests/units/README.md b/fec/tests/units/README.md index c5ac55b..96134df 100644 --- a/fec/tests/units/README.md +++ b/fec/tests/units/README.md @@ -1,26 +1,30 @@ -# M8 fixtures +# 단위 모듈 -M8 is the first multi-unit milestone, so cases live in subdirectories. Each case is compiled separately with that directory on the import path. +이 디렉터리는 `unit` 선언, `import` 경로/별칭, 다중 파일 단위 경계를 다룹니다. -Pass cases: -- `basic`: public function across units. -- `pubfld`: public type and public field across units. -- `dotted` and `alias`: canonical dotted unit paths, last-segment binding, and explicit import aliases. -- `dotpriv`: a dotted unit prefix does not grant access to another unit's private declarations. -- `errsame`: two units use the same anonymous `error.Name`; the driver must assign one deterministic `core.Error` code. -- `errdet`: the same anonymous error-name set appears in a different source/import order; generated `fe_errors.h` must be byte-identical to `errsame` modulo the intentionally different unit graph. +실행 방법: -Fail cases: -- `privfn`: private declaration access. -- `privfld`: private field access. -- `missing`: unresolved import. -- `cycle`: cyclic imports. -- `unitbad`: filename/unit-name mismatch. -- `badupper` and `badlong`: DOS-safe lowercase, eight-character unit-segment limit. -- `bindconf`: two imports with the same last-segment binding require an alias. -- `pubpriv`: a public signature cannot expose a private nominal type. -- `errnom`: nominal error cannot flow into `core.Error` via `try`. +`uv run python tests/run.py -k units` -They are not registered in `src/ferrolang_vm/registry.py` yet. M8 should add procedural checks for `.fei` creation/hash invalidation and deterministic `fe_errors.h` using these fixtures. +현재 수록 케이스: -The M8 DOS gate must additionally construct two separate `-I` roots containing the same canonical unit and require an ambiguity error; repeat the case with two paths to the same canonical file and require deduplication. It must also verify that `std.*` resolves only from the built-in std root, ordinary user units never do, and that changing a private non-generic implementation preserves the dependent interface-cache hit. Those checks need temporary roots/cache inspection and deliberately remain procedural rather than encoding host paths in fixtures. +- `alias/` +- `basic/` +- `badlong/` +- `badupper/` +- `bindconf/` +- `cycle/` +- `dotpriv/` +- `dotted/` +- `errdet/` +- `errnom/` +- `errsame/` +- `missing/` +- `privfld/` +- `privfn/` +- `pubfld/` +- `pubpriv/` +- `unitbad/` + +미구현/미완료 구간: +현재 다수 케이스가 완전 통과하지 않고, `badlong`, `badupper`, `unitbad`는 규칙 고의 위반 케이스입니다. From fe5f853febe60c7938736d95389827102cd3d4ad Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:05:15 +0900 Subject: [PATCH 117/184] =?UTF-8?q?grammar.js=EB=A5=BC=20parser=20?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=EA=B7=9C=EC=B9=99=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=A0=95=ED=95=A9=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- grammar.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/grammar.js b/grammar.js index 910bb6e..adb33ec 100644 --- a/grammar.js +++ b/grammar.js @@ -1,5 +1,5 @@ // Minimal Tree-sitter grammar for Ferro syntax highlighting in Zed. -// This intentionally models lexical structure rather than full Ferro semantics. +// `parser.c` is normative; this file is the editor-copy. module.exports = grammar({ name: "ferro", @@ -10,6 +10,8 @@ module.exports = grammar({ rules: { source_file: $ => repeat(choice( + $.unit_decl, + $.import_decl, $.line_comment, $.block_comment, $.string_literal, @@ -25,6 +27,11 @@ module.exports = grammar({ line_comment: _ => token(seq("//", /[^\n]*/)), + unit_decl: $ => seq("unit", $.dotted_identifier, ";"), + + import_decl: $ => seq("import", $.dotted_identifier, + optional(seq("as", $.identifier)), ";"), + // Ferro block comments may nest. Keeping this rule recursive makes syntax // highlighting follow the compiler lexer instead of flattening nested /* */. block_comment: $ => seq( @@ -92,9 +99,11 @@ module.exports = grammar({ "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "==", "!=", "<=", ">=", "<<", ">>", "->", "=>", "..", "+%", "-%", "*%", - "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", "^", "~", "!", "?", + "+", "-", "*", "/", "%", "=", "<", ">", "&", "|", "^", "!", "?", )), + dotted_identifier: _ => /[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*/, + punctuation: _ => token(choice( "(", ")", "{", "}", "[", "]", ",", ";", ":", ".", )), From d22964cdb6627990b023fedda1b626808fc4f7d9 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:14:30 +0900 Subject: [PATCH 118/184] =?UTF-8?q?tests:=20fixture=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EB=AA=85=EA=B3=BC=20unit=20=EB=8F=99=EA=B8=B0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fec/tests/format/{bad_bufw.fe => badfbuf.fe} | 3 ++- fec/tests/format/{bad_cls.fe => badfcls.fe} | 3 ++- fec/tests/format/{bad_many.fe => badfmany.fe} | 3 ++- fec/tests/format/{bad_ari.fe => badfmta.fe} | 3 ++- fec/tests/format/{bad_open.fe => badfopen.fe} | 3 ++- fec/tests/format/{bad_run.fe => badfrun.fe} | 3 ++- fec/tests/format/{bad_try.fe => badftry.fe} | 3 ++- fec/tests/format/{bad_type.fe => badftype.fe} | 3 ++- fec/tests/format/{bad_verb.fe => badfverb.fe} | 3 ++- fec/tests/format/{bad_writ.fe => badfwrit.fe} | 3 ++- fec/tests/format/{ok_forma.fe => okfmt.fe} | 3 ++- fec/tests/format/{ok_prop.fe => okprop.fe} | 3 ++- fec/tests/format/{ok_try_f.fe => oktryf.fe} | 3 ++- fec/tests/optional/{badcatch.fe => badcatc.fe} | 3 ++- fec/tests/optional/{baddef.fe => baddefi.fe} | 4 +++- fec/tests/optional/{badercod.fe => badecod.fe} | 3 ++- fec/tests/optional/{badernam.fe => badenam.fe} | 3 ++- fec/tests/optional/{badetype.fe => badetyp.fe} | 4 +++- fec/tests/optional/{baddir.fe => badidir.fe} | 3 ++- fec/tests/optional/{badnull.fe => badnullv.fe} | 4 +++- fec/tests/optional/{badoref.fe => badorefv.fe} | 4 +++- fec/tests/optional/{badorel.fe => badorlx.fe} | 4 +++- fec/tests/optional/{badproj.fe => badprjv.fe} | 4 +++- fec/tests/optional/{badqmark.fe => badqmkv.fe} | 4 +++- fec/tests/optional/{badret.fe => badrett.fe} | 3 ++- fec/tests/optional/{badsome.fe => badsomev.fe} | 4 +++- fec/tests/optional/{badtry.fe => badtryx.fe} | 4 +++- fec/tests/optional/{badzero.fe => badzero1.fe} | 4 +++- fec/tests/optional/{okcatch.fe => okcatc.fe} | 3 ++- fec/tests/optional/{okcatmov.fe => okcatmv.fe} | 3 ++- fec/tests/optional/{okcvoid.fe => okcvd.fe} | 3 ++- fec/tests/optional/{okdeflt.fe => okdefl.fe} | 3 ++- fec/tests/optional/{okiflet.fe => okiflt.fe} | 4 +++- fec/tests/optional/{okmatch.fe => okmtch.fe} | 4 +++- fec/tests/optional/{oknull.fe => oknull1.fe} | 4 +++- fec/tests/optional/{okorelse.fe => okorel.fe} | 3 ++- fec/tests/optional/{okproj.fe => okproj1.fe} | 4 +++- fec/tests/optional/{okpatvw.fe => okptvw.fe} | 4 +++- fec/tests/optional/{okrepl.fe => okrepl2.fe} | 4 +++- fec/tests/optional/{oktrdef.fe => oktrd2.fe} | 4 +++- fec/tests/optional/{oktry.fe => oktry2.fe} | 4 +++- fec/tests/own/{badarg.fe => badargu.fe} | 4 +++- fec/tests/own/{badbrmov.fe => badbrmv.fe} | 3 ++- fec/tests/own/{bad_clos.fe => badclos.fe} | 3 ++- fec/tests/own/{bad_cond.fe => badcond.fe} | 3 ++- fec/tests/own/{bad_dbl.fe => baddbl.fe} | 3 ++- fec/tests/own/{baddefer.fe => baddefr.fe} | 3 ++- fec/tests/own/{bad_dest.fe => baddest.fe} | 3 ++- fec/tests/own/{bad_drop.fe => baddrop.fe} | 3 ++- fec/tests/own/{badfld.fe => badfmem.fe} | 3 ++- fec/tests/own/{badglob.fe => badglobx.fe} | 4 +++- fec/tests/own/{badbinit.fe => badinit.fe} | 3 ++- fec/tests/own/{badinv.fe => badinvr.fe} | 4 +++- fec/tests/own/{bad_loop.fe => badlop1.fe} | 3 ++- fec/tests/own/{badloop.fe => badlp2.fe} | 3 ++- fec/tests/own/{badmut.fe => badmutv.fe} | 4 +++- fec/tests/own/{badmut2.fe => badmutx.fe} | 4 +++- fec/tests/own/{bad_move.fe => badmv1.fe} | 3 ++- fec/tests/own/{badmove.fe => badmv2.fe} | 3 ++- fec/tests/own/{bad_proj.fe => badproj.fe} | 3 ++- fec/tests/own/{badptr.fe => badptrx.fe} | 4 +++- fec/tests/own/{badret.fe => badretv.fe} | 3 ++- fec/tests/own/{badrfld.fe => badrfie.fe} | 4 +++- fec/tests/own/{badridx.fe => badri2.fe} | 4 +++- fec/tests/own/{badscop.fe => badscp1.fe} | 4 +++- fec/tests/own/{badself.fe => badselfw.fe} | 4 +++- fec/tests/own/{badshwr.fe => badshw1.fe} | 4 +++- fec/tests/own/{badtwo.fe => badtwo2.fe} | 4 +++- fec/tests/own/{badup.fe => badup1.fe} | 4 +++- fec/tests/own/{badweak.fe => badweak2.fe} | 4 +++- fec/tests/own/{okbranch.fe => okbrch.fe} | 4 +++- fec/tests/own/{ok_defer.fe => okdefer1.fe} | 3 ++- fec/tests/own/{okdefer.fe => okdefer2.fe} | 3 ++- fec/tests/own/{okglobcp.fe => okglobc.fe} | 3 ++- fec/tests/own/{oklast.fe => oklast1.fe} | 4 +++- fec/tests/own/{ok_owned.fe => okowned.fe} | 3 ++- fec/tests/own/{okr8free.fe => okr8fr.fe} | 4 +++- fec/tests/own/{okr8join.fe => okr8jn.fe} | 4 +++- fec/tests/own/{okr8meth.fe => okr8mt.fe} | 4 +++- fec/tests/own/{okr8stat.fe => okr8st.fe} | 4 +++- fec/tests/own/{okrebor.fe => okrbor1.fe} | 4 +++- fec/tests/own/{okrtlast.fe => okrtls.fe} | 4 +++- fec/tests/own/{okshare.fe => okshar1.fe} | 4 +++- fec/tests/own/{okslreb.fe => okslre1.fe} | 4 +++- fec/tests/own/{okstatic.fe => okstat1.fe} | 4 +++- fec/tests/own/{oktemp.fe => oktemp1.fe} | 4 +++- fec/tests/own/{oktrim.fe => oktrim1.fe} | 4 +++- fec/tests/own/{okwcall.fe => okwcal1.fe} | 4 +++- fec/tests/types/{bad_ari.fe => badarith.fe} | 3 ++- fec/tests/types/{badarr.fe => badarry.fe} | 3 ++- fec/tests/types/{bad_asgn.fe => badasgn.fe} | 3 ++- fec/tests/types/{badchar.fe => badbyte.fe} | 3 ++- fec/tests/types/{bad_cast.fe => badcast.fe} | 3 ++- fec/tests/types/{bad_cond.fe => badcond.fe} | 3 ++- fec/tests/types/{badcycle.fe => badcyc.fe} | 4 +++- fec/tests/types/{badfield.fe => badfiel.fe} | 3 ++- fec/tests/types/{badfld.fe => badfmem.fe} | 3 ++- fec/tests/types/{badindex.fe => badidx.fe} | 3 ++- fec/tests/types/{badmat.fe => badmatch.fe} | 3 ++- fec/tests/types/{bad_mlet.fe => badmlet.fe} | 3 ++- fec/tests/types/{bad_ret.fe => badretu.fe} | 3 ++- fec/tests/types/{bad_shwr.fe => badshwr.fe} | 3 ++- fec/tests/types/{badstr.fe => badstrg.fe} | 3 ++- fec/tests/types/{bad_type.fe => badtype.fe} | 3 ++- fec/tests/types/{bad_unit.fe => badunit.fe} | 4 +++- fec/tests/types/{bad_unk.fe => badunk.fe} | 3 ++- fec/tests/types/{bad_void.fe => badvoid.fe} | 3 ++- fec/tests/types/{ok_arra1.fe => okarra1.fe} | 3 ++- fec/tests/types/{ok_array.fe => okarray.fe} | 3 ++- fec/tests/types/{ok_castw.fe => okcastw.fe} | 3 ++- fec/tests/types/{ok_char.fe => okcharc.fe} | 3 ++- fec/tests/types/{ok_enum.fe => okenum.fe} | 3 ++- fec/tests/types/{ok_for.fe => okfor.fe} | 3 ++- fec/tests/types/{ok_hello.fe => okhello.fe} | 3 ++- fec/tests/types/{ok_mutab.fe => okmutab.fe} | 3 ++- fec/tests/types/{ok_neste.fe => okneste.fe} | 3 ++- fec/tests/types/{ok_scope.fe => okscope.fe} | 3 ++- fec/tests/types/{ok_str.fe => okstrg.fe} | 3 ++- fec/tests/types/{ok_struc.fe => okstruc.fe} | 3 ++- 119 files changed, 286 insertions(+), 119 deletions(-) rename fec/tests/format/{bad_bufw.fe => badfbuf.fe} (89%) rename fec/tests/format/{bad_cls.fe => badfcls.fe} (78%) rename fec/tests/format/{bad_many.fe => badfmany.fe} (78%) rename fec/tests/format/{bad_ari.fe => badfmta.fe} (80%) rename fec/tests/format/{bad_open.fe => badfopen.fe} (77%) rename fec/tests/format/{bad_run.fe => badfrun.fe} (84%) rename fec/tests/format/{bad_try.fe => badftry.fe} (80%) rename fec/tests/format/{bad_type.fe => badftype.fe} (87%) rename fec/tests/format/{bad_verb.fe => badfverb.fe} (78%) rename fec/tests/format/{bad_writ.fe => badfwrit.fe} (83%) rename fec/tests/format/{ok_forma.fe => okfmt.fe} (98%) rename fec/tests/format/{ok_prop.fe => okprop.fe} (84%) rename fec/tests/format/{ok_try_f.fe => oktryf.fe} (91%) rename fec/tests/optional/{badcatch.fe => badcatc.fe} (92%) rename fec/tests/optional/{baddef.fe => baddefi.fe} (89%) rename fec/tests/optional/{badercod.fe => badecod.fe} (80%) rename fec/tests/optional/{badernam.fe => badenam.fe} (80%) rename fec/tests/optional/{badetype.fe => badetyp.fe} (90%) rename fec/tests/optional/{baddir.fe => badidir.fe} (87%) rename fec/tests/optional/{badnull.fe => badnullv.fe} (76%) rename fec/tests/optional/{badoref.fe => badorefv.fe} (76%) rename fec/tests/optional/{badorel.fe => badorlx.fe} (89%) rename fec/tests/optional/{badproj.fe => badprjv.fe} (88%) rename fec/tests/optional/{badqmark.fe => badqmkv.fe} (82%) rename fec/tests/optional/{badret.fe => badrett.fe} (88%) rename fec/tests/optional/{badsome.fe => badsomev.fe} (80%) rename fec/tests/optional/{badtry.fe => badtryx.fe} (88%) rename fec/tests/optional/{badzero.fe => badzero1.fe} (70%) rename fec/tests/optional/{okcatch.fe => okcatc.fe} (92%) rename fec/tests/optional/{okcatmov.fe => okcatmv.fe} (90%) rename fec/tests/optional/{okcvoid.fe => okcvd.fe} (90%) rename fec/tests/optional/{okdeflt.fe => okdefl.fe} (89%) rename fec/tests/optional/{okiflet.fe => okiflt.fe} (86%) rename fec/tests/optional/{okmatch.fe => okmtch.fe} (88%) rename fec/tests/optional/{oknull.fe => oknull1.fe} (90%) rename fec/tests/optional/{okorelse.fe => okorel.fe} (77%) rename fec/tests/optional/{okproj.fe => okproj1.fe} (84%) rename fec/tests/optional/{okpatvw.fe => okptvw.fe} (93%) rename fec/tests/optional/{okrepl.fe => okrepl2.fe} (90%) rename fec/tests/optional/{oktrdef.fe => oktrd2.fe} (90%) rename fec/tests/optional/{oktry.fe => oktry2.fe} (90%) rename fec/tests/own/{badarg.fe => badargu.fe} (90%) rename fec/tests/own/{badbrmov.fe => badbrmv.fe} (89%) rename fec/tests/own/{bad_clos.fe => badclos.fe} (94%) rename fec/tests/own/{bad_cond.fe => badcond.fe} (89%) rename fec/tests/own/{bad_dbl.fe => baddbl.fe} (83%) rename fec/tests/own/{baddefer.fe => baddefr.fe} (92%) rename fec/tests/own/{bad_dest.fe => baddest.fe} (76%) rename fec/tests/own/{bad_drop.fe => baddrop.fe} (90%) rename fec/tests/own/{badfld.fe => badfmem.fe} (78%) rename fec/tests/own/{badglob.fe => badglobx.fe} (84%) rename fec/tests/own/{badbinit.fe => badinit.fe} (90%) rename fec/tests/own/{badinv.fe => badinvr.fe} (88%) rename fec/tests/own/{bad_loop.fe => badlop1.fe} (88%) rename fec/tests/own/{badloop.fe => badlp2.fe} (90%) rename fec/tests/own/{badmut.fe => badmutv.fe} (87%) rename fec/tests/own/{badmut2.fe => badmutx.fe} (89%) rename fec/tests/own/{bad_move.fe => badmv1.fe} (87%) rename fec/tests/own/{badmove.fe => badmv2.fe} (90%) rename fec/tests/own/{bad_proj.fe => badproj.fe} (88%) rename fec/tests/own/{badptr.fe => badptrx.fe} (79%) rename fec/tests/own/{badret.fe => badretv.fe} (83%) rename fec/tests/own/{badrfld.fe => badrfie.fe} (90%) rename fec/tests/own/{badridx.fe => badri2.fe} (90%) rename fec/tests/own/{badscop.fe => badscp1.fe} (92%) rename fec/tests/own/{badself.fe => badselfw.fe} (87%) rename fec/tests/own/{badshwr.fe => badshw1.fe} (90%) rename fec/tests/own/{badtwo.fe => badtwo2.fe} (82%) rename fec/tests/own/{badup.fe => badup1.fe} (89%) rename fec/tests/own/{badweak.fe => badweak2.fe} (87%) rename fec/tests/own/{okbranch.fe => okbrch.fe} (93%) rename fec/tests/own/{ok_defer.fe => okdefer1.fe} (80%) rename fec/tests/own/{okdefer.fe => okdefer2.fe} (92%) rename fec/tests/own/{okglobcp.fe => okglobc.fe} (89%) rename fec/tests/own/{oklast.fe => oklast1.fe} (86%) rename fec/tests/own/{ok_owned.fe => okowned.fe} (91%) rename fec/tests/own/{okr8free.fe => okr8fr.fe} (92%) rename fec/tests/own/{okr8join.fe => okr8jn.fe} (94%) rename fec/tests/own/{okr8meth.fe => okr8mt.fe} (93%) rename fec/tests/own/{okr8stat.fe => okr8st.fe} (73%) rename fec/tests/own/{okrebor.fe => okrbor1.fe} (90%) rename fec/tests/own/{okrtlast.fe => okrtls.fe} (91%) rename fec/tests/own/{okshare.fe => okshar1.fe} (90%) rename fec/tests/own/{okslreb.fe => okslre1.fe} (91%) rename fec/tests/own/{okstatic.fe => okstat1.fe} (86%) rename fec/tests/own/{oktemp.fe => oktemp1.fe} (89%) rename fec/tests/own/{oktrim.fe => oktrim1.fe} (78%) rename fec/tests/own/{okwcall.fe => okwcal1.fe} (91%) rename fec/tests/types/{bad_ari.fe => badarith.fe} (85%) rename fec/tests/types/{badarr.fe => badarry.fe} (82%) rename fec/tests/types/{bad_asgn.fe => badasgn.fe} (84%) rename fec/tests/types/{badchar.fe => badbyte.fe} (79%) rename fec/tests/types/{bad_cast.fe => badcast.fe} (81%) rename fec/tests/types/{bad_cond.fe => badcond.fe} (79%) rename fec/tests/types/{badcycle.fe => badcyc.fe} (82%) rename fec/tests/types/{badfield.fe => badfiel.fe} (89%) rename fec/tests/types/{badfld.fe => badfmem.fe} (87%) rename fec/tests/types/{badindex.fe => badidx.fe} (84%) rename fec/tests/types/{badmat.fe => badmatch.fe} (87%) rename fec/tests/types/{bad_mlet.fe => badmlet.fe} (84%) rename fec/tests/types/{bad_ret.fe => badretu.fe} (72%) rename fec/tests/types/{bad_shwr.fe => badshwr.fe} (74%) rename fec/tests/types/{badstr.fe => badstrg.fe} (84%) rename fec/tests/types/{bad_type.fe => badtype.fe} (86%) rename fec/tests/types/{bad_unit.fe => badunit.fe} (78%) rename fec/tests/types/{bad_unk.fe => badunk.fe} (77%) rename fec/tests/types/{bad_void.fe => badvoid.fe} (86%) rename fec/tests/types/{ok_arra1.fe => okarra1.fe} (89%) rename fec/tests/types/{ok_array.fe => okarray.fe} (92%) rename fec/tests/types/{ok_castw.fe => okcastw.fe} (91%) rename fec/tests/types/{ok_char.fe => okcharc.fe} (91%) rename fec/tests/types/{ok_enum.fe => okenum.fe} (96%) rename fec/tests/types/{ok_for.fe => okfor.fe} (97%) rename fec/tests/types/{ok_hello.fe => okhello.fe} (95%) rename fec/tests/types/{ok_mutab.fe => okmutab.fe} (93%) rename fec/tests/types/{ok_neste.fe => okneste.fe} (92%) rename fec/tests/types/{ok_scope.fe => okscope.fe} (94%) rename fec/tests/types/{ok_str.fe => okstrg.fe} (89%) rename fec/tests/types/{ok_struc.fe => okstruc.fe} (96%) diff --git a/fec/tests/format/bad_bufw.fe b/fec/tests/format/badfbuf.fe similarity index 89% rename from fec/tests/format/bad_bufw.fe rename to fec/tests/format/badfbuf.fe index 37bf521..2267cb8 100644 --- a/fec/tests/format/bad_bufw.fe +++ b/fec/tests/format/badfbuf.fe @@ -1,7 +1,8 @@ -unit bad_bufw; +unit badfbuf; fn main() -> void { var raw: [4]u8 = [0, 0, 0, 0]; var buf: []mut u8 = raw[..]; let w = io.buf_writer(buf); } + diff --git a/fec/tests/format/bad_cls.fe b/fec/tests/format/badfcls.fe similarity index 78% rename from fec/tests/format/bad_cls.fe rename to fec/tests/format/badfcls.fe index 1f8a720..032d923 100644 --- a/fec/tests/format/bad_cls.fe +++ b/fec/tests/format/badfcls.fe @@ -1,6 +1,7 @@ -unit bad_cls; +unit badfcls; fn main() -> i32 { @print("}", 1); return 0; } + diff --git a/fec/tests/format/bad_many.fe b/fec/tests/format/badfmany.fe similarity index 78% rename from fec/tests/format/bad_many.fe rename to fec/tests/format/badfmany.fe index ab25401..044e758 100644 --- a/fec/tests/format/bad_many.fe +++ b/fec/tests/format/badfmany.fe @@ -1,6 +1,7 @@ -unit bad_many; +unit badfmany; fn main() -> i32 { @print("{}", 1, 2); return 0; } + diff --git a/fec/tests/format/bad_ari.fe b/fec/tests/format/badfmta.fe similarity index 80% rename from fec/tests/format/bad_ari.fe rename to fec/tests/format/badfmta.fe index 7dc30b8..01534cb 100644 --- a/fec/tests/format/bad_ari.fe +++ b/fec/tests/format/badfmta.fe @@ -1,6 +1,7 @@ -unit bad_ari; +unit badfmta; fn main() -> i32 { @print("{} {}", 1); return 0; } + diff --git a/fec/tests/format/bad_open.fe b/fec/tests/format/badfopen.fe similarity index 77% rename from fec/tests/format/bad_open.fe rename to fec/tests/format/badfopen.fe index 98ee2c8..6b32d2b 100644 --- a/fec/tests/format/bad_open.fe +++ b/fec/tests/format/badfopen.fe @@ -1,6 +1,7 @@ -unit bad_open; +unit badfopen; fn main() -> i32 { @print("{", 1); return 0; } + diff --git a/fec/tests/format/bad_run.fe b/fec/tests/format/badfrun.fe similarity index 84% rename from fec/tests/format/bad_run.fe rename to fec/tests/format/badfrun.fe index c7945e1..eed67b9 100644 --- a/fec/tests/format/bad_run.fe +++ b/fec/tests/format/badfrun.fe @@ -1,7 +1,8 @@ -unit bad_run; +unit badfrun; fn main() -> i32 { var fmt: str = "{}"; @print(fmt, 1); return 0; } + diff --git a/fec/tests/format/bad_try.fe b/fec/tests/format/badftry.fe similarity index 80% rename from fec/tests/format/bad_try.fe rename to fec/tests/format/badftry.fe index 0c5da62..e3fb5f6 100644 --- a/fec/tests/format/bad_try.fe +++ b/fec/tests/format/badftry.fe @@ -1,6 +1,7 @@ -unit bad_try; +unit badftry; fn main() -> i32 { try @print("nope"); return 0; } + diff --git a/fec/tests/format/bad_type.fe b/fec/tests/format/badftype.fe similarity index 87% rename from fec/tests/format/bad_type.fe rename to fec/tests/format/badftype.fe index 9279e47..72c4fbb 100644 --- a/fec/tests/format/bad_type.fe +++ b/fec/tests/format/badftype.fe @@ -1,4 +1,4 @@ -unit bad_type; +unit badftype; struct Point { x: i32, } @@ -7,3 +7,4 @@ fn main() -> i32 { @print("{}", p); return 0; } + diff --git a/fec/tests/format/bad_verb.fe b/fec/tests/format/badfverb.fe similarity index 78% rename from fec/tests/format/bad_verb.fe rename to fec/tests/format/badfverb.fe index 9a928ce..51e5308 100644 --- a/fec/tests/format/bad_verb.fe +++ b/fec/tests/format/badfverb.fe @@ -1,6 +1,7 @@ -unit bad_verb; +unit badfverb; fn main() -> i32 { @print("{q}", 1); return 0; } + diff --git a/fec/tests/format/bad_writ.fe b/fec/tests/format/badfwrit.fe similarity index 83% rename from fec/tests/format/bad_writ.fe rename to fec/tests/format/badfwrit.fe index 18e33a6..f5f84a9 100644 --- a/fec/tests/format/bad_writ.fe +++ b/fec/tests/format/badfwrit.fe @@ -1,7 +1,8 @@ -unit bad_writ; +unit badfwrit; fn main() -> i32 { var x: i32 = 0; @fprint(x, "bad"); return 0; } + diff --git a/fec/tests/format/ok_forma.fe b/fec/tests/format/okfmt.fe similarity index 98% rename from fec/tests/format/ok_forma.fe rename to fec/tests/format/okfmt.fe index a830b93..5cde15b 100644 --- a/fec/tests/format/ok_forma.fe +++ b/fec/tests/format/okfmt.fe @@ -1,4 +1,4 @@ -unit ok_forma; +unit okfmt; const FMT: str = "n={} hex={x} c={c} s={s} b={b} {{ok}}\n"; @@ -28,3 +28,4 @@ fn main() -> i32 { buf5[0] == ('n' as u8) and buf5[2] == ('2' as u8) { return 0; } return 1; } + diff --git a/fec/tests/format/ok_prop.fe b/fec/tests/format/okprop.fe similarity index 84% rename from fec/tests/format/ok_prop.fe rename to fec/tests/format/okprop.fe index 49a0051..09bd3b0 100644 --- a/fec/tests/format/ok_prop.fe +++ b/fec/tests/format/okprop.fe @@ -1,5 +1,6 @@ -unit ok_prop; +unit okprop; pub fn propagate(w: io.Writer) -> !void { try @fprint(w, "a{}b", 1); } + diff --git a/fec/tests/format/ok_try_f.fe b/fec/tests/format/oktryf.fe similarity index 91% rename from fec/tests/format/ok_try_f.fe rename to fec/tests/format/oktryf.fe index 5429146..9dbb866 100644 --- a/fec/tests/format/ok_try_f.fe +++ b/fec/tests/format/oktryf.fe @@ -1,4 +1,4 @@ -unit ok_try_f; +unit oktryf; fn main() -> !void { var raw: [4]u8 = [0, 0, 0, 0]; @@ -6,3 +6,4 @@ fn main() -> !void { let w: io.Writer = io.null_writer(); try @fprint(w, "ok"); } + diff --git a/fec/tests/optional/badcatch.fe b/fec/tests/optional/badcatc.fe similarity index 92% rename from fec/tests/optional/badcatch.fe rename to fec/tests/optional/badcatc.fe index 97ed75c..01966ee 100644 --- a/fec/tests/optional/badcatch.fe +++ b/fec/tests/optional/badcatc.fe @@ -1,5 +1,5 @@ // ERROR:13:catch -unit badcatch; +unit badcatc; error E { Bad = 1, @@ -15,3 +15,4 @@ fn bad() -> i32 { }; return v; } + diff --git a/fec/tests/optional/baddef.fe b/fec/tests/optional/baddefi.fe similarity index 89% rename from fec/tests/optional/baddef.fe rename to fec/tests/optional/baddefi.fe index 2ff2b0f..2ce9671 100644 --- a/fec/tests/optional/baddef.fe +++ b/fec/tests/optional/baddefi.fe @@ -1,5 +1,5 @@ // ERROR:13:type -unit baddef; +unit baddefi; error E { Bad = 1, @@ -12,3 +12,5 @@ fn leaf() -> E!i32 { fn bad() -> i32 { return leaf() catch false; } + + diff --git a/fec/tests/optional/badercod.fe b/fec/tests/optional/badecod.fe similarity index 80% rename from fec/tests/optional/badercod.fe rename to fec/tests/optional/badecod.fe index ad5b34c..9f4e355 100644 --- a/fec/tests/optional/badercod.fe +++ b/fec/tests/optional/badecod.fe @@ -1,7 +1,8 @@ // ERROR:6:duplicate -unit badercod; +unit badecod; error E { One = 1, Two = 1, } + diff --git a/fec/tests/optional/badernam.fe b/fec/tests/optional/badenam.fe similarity index 80% rename from fec/tests/optional/badernam.fe rename to fec/tests/optional/badenam.fe index 661e8c0..dc24789 100644 --- a/fec/tests/optional/badernam.fe +++ b/fec/tests/optional/badenam.fe @@ -1,7 +1,8 @@ // ERROR:6:duplicate -unit badernam; +unit badenam; error E { One = 1, One = 2, } + diff --git a/fec/tests/optional/badetype.fe b/fec/tests/optional/badetyp.fe similarity index 90% rename from fec/tests/optional/badetype.fe rename to fec/tests/optional/badetyp.fe index af5bf39..d847230 100644 --- a/fec/tests/optional/badetype.fe +++ b/fec/tests/optional/badetyp.fe @@ -1,5 +1,5 @@ // ERROR:17:error -unit badetype; +unit badetyp; error A { Bad = 1, @@ -16,3 +16,5 @@ fn leaf() -> A!i32 { fn bad() -> B!i32 { return try leaf(); } + + diff --git a/fec/tests/optional/baddir.fe b/fec/tests/optional/badidir.fe similarity index 87% rename from fec/tests/optional/baddir.fe rename to fec/tests/optional/badidir.fe index 46e0715..6d4efde 100644 --- a/fec/tests/optional/baddir.fe +++ b/fec/tests/optional/badidir.fe @@ -1,5 +1,5 @@ // ERROR:9:optional -unit baddir; +unit badidir; struct Node { value: i32, @@ -8,3 +8,4 @@ struct Node { fn bad(p: ?^Node) -> i32 { return p.^.value; } + diff --git a/fec/tests/optional/badnull.fe b/fec/tests/optional/badnullv.fe similarity index 76% rename from fec/tests/optional/badnull.fe rename to fec/tests/optional/badnullv.fe index 4ed896b..d2ffae9 100644 --- a/fec/tests/optional/badnull.fe +++ b/fec/tests/optional/badnullv.fe @@ -1,6 +1,8 @@ // ERROR:5:null -unit badnull; +unit badnullv; fn bad() -> void { let p = null; } + + diff --git a/fec/tests/optional/badoref.fe b/fec/tests/optional/badorefv.fe similarity index 76% rename from fec/tests/optional/badoref.fe rename to fec/tests/optional/badorefv.fe index 641206e..150e1c6 100644 --- a/fec/tests/optional/badoref.fe +++ b/fec/tests/optional/badorefv.fe @@ -1,6 +1,8 @@ // ERROR:5:reference -unit badoref; +unit badorefv; struct Bad { value: ?&i32, } + + diff --git a/fec/tests/optional/badorel.fe b/fec/tests/optional/badorlx.fe similarity index 89% rename from fec/tests/optional/badorel.fe rename to fec/tests/optional/badorlx.fe index 3a79145..56c3b6a 100644 --- a/fec/tests/optional/badorel.fe +++ b/fec/tests/optional/badorlx.fe @@ -1,5 +1,5 @@ // ERROR:9:non-Copy -unit badorel; +unit badorlx; struct Node { next: ?^Node, @@ -8,3 +8,5 @@ struct Node { fn bad(p: ^Node, fallback: ^Node) -> ^Node { return p.next orelse fallback; } + + diff --git a/fec/tests/optional/badproj.fe b/fec/tests/optional/badprjv.fe similarity index 88% rename from fec/tests/optional/badproj.fe rename to fec/tests/optional/badprjv.fe index a7d987c..24525d2 100644 --- a/fec/tests/optional/badproj.fe +++ b/fec/tests/optional/badprjv.fe @@ -1,5 +1,5 @@ // ERROR:9:mem.replace -unit badproj; +unit badprjv; struct Node { next: ?^Node, @@ -9,3 +9,5 @@ fn bad(p: ^Node) -> void { let q = p.next; mem.destroy(p); } + + diff --git a/fec/tests/optional/badqmark.fe b/fec/tests/optional/badqmkv.fe similarity index 82% rename from fec/tests/optional/badqmark.fe rename to fec/tests/optional/badqmkv.fe index 36cdc7b..183a5d2 100644 --- a/fec/tests/optional/badqmark.fe +++ b/fec/tests/optional/badqmkv.fe @@ -1,7 +1,9 @@ // ERROR:6:optional -unit badqmark; +unit badqmkv; fn bad() -> i32 { let x: i32 = 1; return x.?; } + + diff --git a/fec/tests/optional/badret.fe b/fec/tests/optional/badrett.fe similarity index 88% rename from fec/tests/optional/badret.fe rename to fec/tests/optional/badrett.fe index 84842e2..adab03a 100644 --- a/fec/tests/optional/badret.fe +++ b/fec/tests/optional/badrett.fe @@ -1,5 +1,5 @@ // ERROR:13:error -unit badret; +unit badrett; error A { Bad = 1, @@ -12,3 +12,4 @@ error B { fn bad() -> B!i32 { return A.Bad; } + diff --git a/fec/tests/optional/badsome.fe b/fec/tests/optional/badsomev.fe similarity index 80% rename from fec/tests/optional/badsome.fe rename to fec/tests/optional/badsomev.fe index f35a461..f12958c 100644 --- a/fec/tests/optional/badsome.fe +++ b/fec/tests/optional/badsomev.fe @@ -1,7 +1,9 @@ // ERROR:5:Some -unit badsome; +unit badsomev; fn bad() -> i32 { let x = Some(1); return x; } + + diff --git a/fec/tests/optional/badtry.fe b/fec/tests/optional/badtryx.fe similarity index 88% rename from fec/tests/optional/badtry.fe rename to fec/tests/optional/badtryx.fe index a41273e..bd5ee9b 100644 --- a/fec/tests/optional/badtry.fe +++ b/fec/tests/optional/badtryx.fe @@ -1,5 +1,5 @@ // ERROR:13:try -unit badtry; +unit badtryx; error E { Bad = 1, @@ -12,3 +12,5 @@ fn leaf() -> E!i32 { fn bad() -> i32 { return try leaf(); } + + diff --git a/fec/tests/optional/badzero.fe b/fec/tests/optional/badzero1.fe similarity index 70% rename from fec/tests/optional/badzero.fe rename to fec/tests/optional/badzero1.fe index da86afb..b51d040 100644 --- a/fec/tests/optional/badzero.fe +++ b/fec/tests/optional/badzero1.fe @@ -1,6 +1,8 @@ // ERROR:5:0 -unit badzero; +unit badzero1; error E { Zero = 0, } + + diff --git a/fec/tests/optional/okcatch.fe b/fec/tests/optional/okcatc.fe similarity index 92% rename from fec/tests/optional/okcatch.fe rename to fec/tests/optional/okcatc.fe index 557fd12..13aa727 100644 --- a/fec/tests/optional/okcatch.fe +++ b/fec/tests/optional/okcatc.fe @@ -1,4 +1,4 @@ -unit okcatch; +unit okcatc; error E { Bad = 1, @@ -14,3 +14,4 @@ fn top() -> E!i32 { }; return v; } + diff --git a/fec/tests/optional/okcatmov.fe b/fec/tests/optional/okcatmv.fe similarity index 90% rename from fec/tests/optional/okcatmov.fe rename to fec/tests/optional/okcatmv.fe index c2c7b37..9cdc393 100644 --- a/fec/tests/optional/okcatmov.fe +++ b/fec/tests/optional/okcatmv.fe @@ -1,4 +1,4 @@ -unit okcatmov; +unit okcatmv; error E { Bad = 1, } struct Node { value: i32, } @@ -6,3 +6,4 @@ struct Node { value: i32, } fn recover(result: E!^Node, fallback: ^Node) -> ^Node { return result catch fallback; } + diff --git a/fec/tests/optional/okcvoid.fe b/fec/tests/optional/okcvd.fe similarity index 90% rename from fec/tests/optional/okcvoid.fe rename to fec/tests/optional/okcvd.fe index 4c1c2ab..1fab046 100644 --- a/fec/tests/optional/okcvoid.fe +++ b/fec/tests/optional/okcvd.fe @@ -1,4 +1,4 @@ -unit okcvoid; +unit okcvd; error E { Bad = 1, @@ -13,3 +13,4 @@ fn top() -> void { return; }; } + diff --git a/fec/tests/optional/okdeflt.fe b/fec/tests/optional/okdefl.fe similarity index 89% rename from fec/tests/optional/okdeflt.fe rename to fec/tests/optional/okdefl.fe index b62d66c..b5fa8a6 100644 --- a/fec/tests/optional/okdeflt.fe +++ b/fec/tests/optional/okdefl.fe @@ -1,4 +1,4 @@ -unit okdeflt; +unit okdefl; error E { Bad = 1, @@ -11,3 +11,4 @@ fn leaf() -> E!i32 { fn top() -> i32 { return leaf() catch 11; } + diff --git a/fec/tests/optional/okiflet.fe b/fec/tests/optional/okiflt.fe similarity index 86% rename from fec/tests/optional/okiflet.fe rename to fec/tests/optional/okiflt.fe index 3192c4b..7d9d14c 100644 --- a/fec/tests/optional/okiflet.fe +++ b/fec/tests/optional/okiflt.fe @@ -1,4 +1,4 @@ -unit okiflet; +unit okiflt; fn value(p: ?i32) -> i32 { if let Some(v) = p { @@ -6,3 +6,5 @@ fn value(p: ?i32) -> i32 { } return 0; } + + diff --git a/fec/tests/optional/okmatch.fe b/fec/tests/optional/okmtch.fe similarity index 88% rename from fec/tests/optional/okmatch.fe rename to fec/tests/optional/okmtch.fe index 6335cff..6cfaa8d 100644 --- a/fec/tests/optional/okmatch.fe +++ b/fec/tests/optional/okmtch.fe @@ -1,4 +1,4 @@ -unit okmatch; +unit okmtch; fn value(p: ?i32) -> i32 { match p { @@ -6,3 +6,5 @@ fn value(p: ?i32) -> i32 { None => { return 0; } } } + + diff --git a/fec/tests/optional/oknull.fe b/fec/tests/optional/oknull1.fe similarity index 90% rename from fec/tests/optional/oknull.fe rename to fec/tests/optional/oknull1.fe index c12a917..ce22d95 100644 --- a/fec/tests/optional/oknull.fe +++ b/fec/tests/optional/oknull1.fe @@ -1,4 +1,4 @@ -unit oknull; +unit oknull1; struct Node { value: i32, } @@ -8,3 +8,5 @@ fn test() -> bool { let p: ?^Node = null; return accepts(p); } + + diff --git a/fec/tests/optional/okorelse.fe b/fec/tests/optional/okorel.fe similarity index 77% rename from fec/tests/optional/okorelse.fe rename to fec/tests/optional/okorel.fe index 9ff537e..f49a0f5 100644 --- a/fec/tests/optional/okorelse.fe +++ b/fec/tests/optional/okorel.fe @@ -1,5 +1,6 @@ -unit okorelse; +unit okorel; fn value(p: ?i32) -> i32 { return p orelse 9; } + diff --git a/fec/tests/optional/okproj.fe b/fec/tests/optional/okproj1.fe similarity index 84% rename from fec/tests/optional/okproj.fe rename to fec/tests/optional/okproj1.fe index f54768a..4bcca85 100644 --- a/fec/tests/optional/okproj.fe +++ b/fec/tests/optional/okproj1.fe @@ -1,4 +1,4 @@ -unit okproj; +unit okproj1; struct Node { value: i32, @@ -7,3 +7,5 @@ struct Node { fn touch(p: ?&mut Node) -> void { p.?.value = 7; } + + diff --git a/fec/tests/optional/okpatvw.fe b/fec/tests/optional/okptvw.fe similarity index 93% rename from fec/tests/optional/okpatvw.fe rename to fec/tests/optional/okptvw.fe index 5797b46..a046b5e 100644 --- a/fec/tests/optional/okpatvw.fe +++ b/fec/tests/optional/okptvw.fe @@ -1,4 +1,4 @@ -unit okpatvw; +unit okptvw; struct Node { value: i32, } @@ -10,3 +10,5 @@ fn keep(p: ?^Node) -> void { let owned = mem.replace(&mut slot, null).?; mem.destroy(owned); } + + diff --git a/fec/tests/optional/okrepl.fe b/fec/tests/optional/okrepl2.fe similarity index 90% rename from fec/tests/optional/okrepl.fe rename to fec/tests/optional/okrepl2.fe index 2710132..1bab0bd 100644 --- a/fec/tests/optional/okrepl.fe +++ b/fec/tests/optional/okrepl2.fe @@ -1,4 +1,4 @@ -unit okrepl; +unit okrepl2; struct Node { value: i32, @@ -9,3 +9,5 @@ fn take(p: ?^Node) -> void { let n = mem.replace(&mut q, null).?; mem.destroy(n); } + + diff --git a/fec/tests/optional/oktrdef.fe b/fec/tests/optional/oktrd2.fe similarity index 90% rename from fec/tests/optional/oktrdef.fe rename to fec/tests/optional/oktrd2.fe index 1b0831c..f0361df 100644 --- a/fec/tests/optional/oktrdef.fe +++ b/fec/tests/optional/oktrd2.fe @@ -1,4 +1,4 @@ -unit oktrdef; +unit oktrd2; error E { Bad = 1, @@ -12,3 +12,5 @@ fn top() -> E!i32 { defer { let x: i32 = 1; } return try leaf(); } + + diff --git a/fec/tests/optional/oktry.fe b/fec/tests/optional/oktry2.fe similarity index 90% rename from fec/tests/optional/oktry.fe rename to fec/tests/optional/oktry2.fe index f0066db..6140223 100644 --- a/fec/tests/optional/oktry.fe +++ b/fec/tests/optional/oktry2.fe @@ -1,4 +1,4 @@ -unit oktry; +unit oktry2; error E { Bad = 1, @@ -12,3 +12,5 @@ fn leaf(ok: bool) -> E!i32 { fn top() -> E!i32 { return try leaf(true); } + + diff --git a/fec/tests/own/badarg.fe b/fec/tests/own/badargu.fe similarity index 90% rename from fec/tests/own/badarg.fe rename to fec/tests/own/badargu.fe index 7e50114..55d092d 100644 --- a/fec/tests/own/badarg.fe +++ b/fec/tests/own/badargu.fe @@ -1,5 +1,5 @@ // ERROR:8:derived from a parameter -unit badarg; +unit badargu; struct Box { value: i32, @@ -8,3 +8,5 @@ struct Box { return other; } } + + diff --git a/fec/tests/own/badbrmov.fe b/fec/tests/own/badbrmv.fe similarity index 89% rename from fec/tests/own/badbrmov.fe rename to fec/tests/own/badbrmv.fe index f07825d..d0fc104 100644 --- a/fec/tests/own/badbrmov.fe +++ b/fec/tests/own/badbrmv.fe @@ -1,5 +1,5 @@ // ERROR:8:move -unit badbrmov; +unit badbrmv; fn bad(p: ^i32, consume: bool) -> void { if consume { @@ -7,3 +7,4 @@ fn bad(p: ^i32, consume: bool) -> void { } mem.destroy(p); } + diff --git a/fec/tests/own/bad_clos.fe b/fec/tests/own/badclos.fe similarity index 94% rename from fec/tests/own/bad_clos.fe rename to fec/tests/own/badclos.fe index 28ff3e4..447c0f6 100644 --- a/fec/tests/own/bad_clos.fe +++ b/fec/tests/own/badclos.fe @@ -1,4 +1,4 @@ -unit bad_clos; +unit badclos; struct FileLike { handle: i32, @@ -11,3 +11,4 @@ fn bad() -> !void { try file.close(); file.close(); } + diff --git a/fec/tests/own/bad_cond.fe b/fec/tests/own/badcond.fe similarity index 89% rename from fec/tests/own/bad_cond.fe rename to fec/tests/own/badcond.fe index f46cfe4..2f5916d 100644 --- a/fec/tests/own/bad_cond.fe +++ b/fec/tests/own/badcond.fe @@ -1,4 +1,4 @@ -unit bad_cond; +unit badcond; fn take(p: ^i32) -> void { mem.destroy(p); } @@ -6,3 +6,4 @@ fn bad(p: ^i32, flag: bool) -> void { if flag { take(p); } p.^ = 3; } + diff --git a/fec/tests/own/bad_dbl.fe b/fec/tests/own/baddbl.fe similarity index 83% rename from fec/tests/own/bad_dbl.fe rename to fec/tests/own/baddbl.fe index a990673..84f3912 100644 --- a/fec/tests/own/bad_dbl.fe +++ b/fec/tests/own/baddbl.fe @@ -1,6 +1,7 @@ -unit bad_dbl; +unit baddbl; fn bad(p: ^i32) -> void { mem.destroy(p); mem.destroy(p); } + diff --git a/fec/tests/own/baddefer.fe b/fec/tests/own/baddefr.fe similarity index 92% rename from fec/tests/own/baddefer.fe rename to fec/tests/own/baddefr.fe index faf7839..47a991f 100644 --- a/fec/tests/own/baddefer.fe +++ b/fec/tests/own/baddefr.fe @@ -1,5 +1,5 @@ // ERROR:10:borrow -unit baddefer; +unit baddefr; fn read(r: &i32) -> i32 { return r.^; } @@ -10,3 +10,4 @@ fn bad() -> i32 { x = 1; return x; } + diff --git a/fec/tests/own/bad_dest.fe b/fec/tests/own/baddest.fe similarity index 76% rename from fec/tests/own/bad_dest.fe rename to fec/tests/own/baddest.fe index 8cedd19..11d462b 100644 --- a/fec/tests/own/bad_dest.fe +++ b/fec/tests/own/baddest.fe @@ -1,5 +1,6 @@ -unit bad_dest; +unit baddest; fn bad(x: i32) -> void { mem.destroy(x); } + diff --git a/fec/tests/own/bad_drop.fe b/fec/tests/own/baddrop.fe similarity index 90% rename from fec/tests/own/bad_drop.fe rename to fec/tests/own/baddrop.fe index ad9e9cc..5a4e74d 100644 --- a/fec/tests/own/bad_drop.fe +++ b/fec/tests/own/baddrop.fe @@ -1,4 +1,4 @@ -unit bad_drop; +unit baddrop; struct Box { value: i32, @@ -9,3 +9,4 @@ fn bad() -> void { var b: Box = Box{ value: 1 }; b.drop(); } + diff --git a/fec/tests/own/badfld.fe b/fec/tests/own/badfmem.fe similarity index 78% rename from fec/tests/own/badfld.fe rename to fec/tests/own/badfmem.fe index d4090ff..71eec2c 100644 --- a/fec/tests/own/badfld.fe +++ b/fec/tests/own/badfmem.fe @@ -1,6 +1,7 @@ // ERROR:5:reference -unit badfld; +unit badfmem; struct Bad { value: &i32, } + diff --git a/fec/tests/own/badglob.fe b/fec/tests/own/badglobx.fe similarity index 84% rename from fec/tests/own/badglob.fe rename to fec/tests/own/badglobx.fe index 41a107c..3afdcc9 100644 --- a/fec/tests/own/badglob.fe +++ b/fec/tests/own/badglobx.fe @@ -1,5 +1,5 @@ // ERROR:7:global -unit badglob; +unit badglobx; var VALUE: i32 = 0; @@ -7,3 +7,5 @@ fn bad() -> i32 { let r = &VALUE; return r.^; } + + diff --git a/fec/tests/own/badbinit.fe b/fec/tests/own/badinit.fe similarity index 90% rename from fec/tests/own/badbinit.fe rename to fec/tests/own/badinit.fe index b6eb632..049641b 100644 --- a/fec/tests/own/badbinit.fe +++ b/fec/tests/own/badinit.fe @@ -1,5 +1,5 @@ // ERROR:9:initialized -unit badbinit; +unit badinit; fn bad(assign: bool) -> i32 { var value: i32; @@ -8,3 +8,4 @@ fn bad(assign: bool) -> i32 { } return value; } + diff --git a/fec/tests/own/badinv.fe b/fec/tests/own/badinvr.fe similarity index 88% rename from fec/tests/own/badinv.fe rename to fec/tests/own/badinvr.fe index 88f483c..3338eed 100644 --- a/fec/tests/own/badinv.fe +++ b/fec/tests/own/badinvr.fe @@ -1,5 +1,5 @@ // ERROR:6:borrow -unit badinv; +unit badinvr; fn bad(p: ^i32, q: ^i32) -> void { let r = &p; @@ -7,3 +7,5 @@ fn bad(p: ^i32, q: ^i32) -> void { let keep = r; mem.destroy(p); } + + diff --git a/fec/tests/own/bad_loop.fe b/fec/tests/own/badlop1.fe similarity index 88% rename from fec/tests/own/bad_loop.fe rename to fec/tests/own/badlop1.fe index c7fd822..7dc06ff 100644 --- a/fec/tests/own/bad_loop.fe +++ b/fec/tests/own/badlop1.fe @@ -1,7 +1,8 @@ -unit bad_loop; +unit badlop1; fn take(p: ^i32) -> void { mem.destroy(p); } fn bad(p: ^i32, again: bool) -> void { while again { take(p); } } + diff --git a/fec/tests/own/badloop.fe b/fec/tests/own/badlp2.fe similarity index 90% rename from fec/tests/own/badloop.fe rename to fec/tests/own/badlp2.fe index 1cb6a4f..0d403b6 100644 --- a/fec/tests/own/badloop.fe +++ b/fec/tests/own/badlp2.fe @@ -1,5 +1,5 @@ // ERROR:6:move -unit badloop; +unit badlp2; fn bad(p: ^i32, again: bool) -> void { while again { @@ -7,3 +7,4 @@ fn bad(p: ^i32, again: bool) -> void { } mem.destroy(p); } + diff --git a/fec/tests/own/badmut.fe b/fec/tests/own/badmutv.fe similarity index 87% rename from fec/tests/own/badmut.fe rename to fec/tests/own/badmutv.fe index 90aab1f..95741c0 100644 --- a/fec/tests/own/badmut.fe +++ b/fec/tests/own/badmutv.fe @@ -1,5 +1,5 @@ // ERROR:7:borrow -unit badmut; +unit badmutv; fn bad() -> i32 { var x: i32 = 0; @@ -8,3 +8,5 @@ fn bad() -> i32 { r.^ = 2; return x; } + + diff --git a/fec/tests/own/badmut2.fe b/fec/tests/own/badmutx.fe similarity index 89% rename from fec/tests/own/badmut2.fe rename to fec/tests/own/badmutx.fe index 790e251..34fa6e9 100644 --- a/fec/tests/own/badmut2.fe +++ b/fec/tests/own/badmutx.fe @@ -1,5 +1,5 @@ // ERROR:7:borrow -unit badmut2; +unit badmutx; fn bad() -> i32 { var x: i32 = 0; @@ -9,3 +9,5 @@ fn bad() -> i32 { b.^ = 2; return x; } + + diff --git a/fec/tests/own/bad_move.fe b/fec/tests/own/badmv1.fe similarity index 87% rename from fec/tests/own/bad_move.fe rename to fec/tests/own/badmv1.fe index 15d25fb..8f53bcc 100644 --- a/fec/tests/own/bad_move.fe +++ b/fec/tests/own/badmv1.fe @@ -1,4 +1,4 @@ -unit bad_move; +unit badmv1; fn take(p: ^i32) -> void { mem.destroy(p); } @@ -6,3 +6,4 @@ fn twice(p: ^i32) -> void { take(p); take(p); } + diff --git a/fec/tests/own/badmove.fe b/fec/tests/own/badmv2.fe similarity index 90% rename from fec/tests/own/badmove.fe rename to fec/tests/own/badmv2.fe index e746062..da71598 100644 --- a/fec/tests/own/badmove.fe +++ b/fec/tests/own/badmv2.fe @@ -1,5 +1,5 @@ // ERROR:8:borrow -unit badmove; +unit badmv2; fn take(p: ^i32) -> void { mem.destroy(p); } @@ -8,3 +8,4 @@ fn bad(p: ^i32) -> void { take(p); let q = r; } + diff --git a/fec/tests/own/bad_proj.fe b/fec/tests/own/badproj.fe similarity index 88% rename from fec/tests/own/bad_proj.fe rename to fec/tests/own/badproj.fe index 0c50987..e756adf 100644 --- a/fec/tests/own/bad_proj.fe +++ b/fec/tests/own/badproj.fe @@ -1,4 +1,4 @@ -unit bad_proj; +unit badproj; struct Holder { p: ^i32 } fn take(p: ^i32) -> void { mem.destroy(p); } @@ -6,3 +6,4 @@ fn take(p: ^i32) -> void { mem.destroy(p); } fn bad(h: Holder) -> void { take(h.p); } + diff --git a/fec/tests/own/badptr.fe b/fec/tests/own/badptrx.fe similarity index 79% rename from fec/tests/own/badptr.fe rename to fec/tests/own/badptrx.fe index 36e9490..1426db3 100644 --- a/fec/tests/own/badptr.fe +++ b/fec/tests/own/badptrx.fe @@ -1,6 +1,8 @@ // ERROR:4:reference -unit badptr; +unit badptrx; fn bad(p: *&i32) -> void { return; } + + diff --git a/fec/tests/own/badret.fe b/fec/tests/own/badretv.fe similarity index 83% rename from fec/tests/own/badret.fe rename to fec/tests/own/badretv.fe index 5a9a777..774a760 100644 --- a/fec/tests/own/badret.fe +++ b/fec/tests/own/badretv.fe @@ -1,7 +1,8 @@ // ERROR:6:reference -unit badret; +unit badretv; fn bad() -> &i32 { let x: i32 = 1; return &x; } + diff --git a/fec/tests/own/badrfld.fe b/fec/tests/own/badrfie.fe similarity index 90% rename from fec/tests/own/badrfld.fe rename to fec/tests/own/badrfie.fe index 65cb8b0..fd366aa 100644 --- a/fec/tests/own/badrfld.fe +++ b/fec/tests/own/badrfie.fe @@ -1,5 +1,5 @@ // ERROR:9:borrow -unit badrfld; +unit badrfie; struct Pair { a: i32, b: i32, } @@ -9,3 +9,5 @@ fn bad() -> void { p.b = 3; left.^ = 4; } + + diff --git a/fec/tests/own/badridx.fe b/fec/tests/own/badri2.fe similarity index 90% rename from fec/tests/own/badridx.fe rename to fec/tests/own/badri2.fe index d7597c5..a6987b3 100644 --- a/fec/tests/own/badridx.fe +++ b/fec/tests/own/badri2.fe @@ -1,5 +1,5 @@ // ERROR:7:borrow -unit badridx; +unit badri2; fn bad() -> void { var xs: [2]i32 = [1, 2]; @@ -8,3 +8,5 @@ fn bad() -> void { a.^ = 3; b.^ = 4; } + + diff --git a/fec/tests/own/badscop.fe b/fec/tests/own/badscp1.fe similarity index 92% rename from fec/tests/own/badscop.fe rename to fec/tests/own/badscp1.fe index 3bbeded..a7893fe 100644 --- a/fec/tests/own/badscop.fe +++ b/fec/tests/own/badscp1.fe @@ -1,5 +1,5 @@ // ERROR:9:reference -unit badscop; +unit badscp1; fn bad(cond: bool) -> i32 { let outer: i32 = 0; @@ -10,3 +10,5 @@ fn bad(cond: bool) -> i32 { } return r.^; } + + diff --git a/fec/tests/own/badself.fe b/fec/tests/own/badselfw.fe similarity index 87% rename from fec/tests/own/badself.fe rename to fec/tests/own/badselfw.fe index fe80c84..2e2795d 100644 --- a/fec/tests/own/badself.fe +++ b/fec/tests/own/badselfw.fe @@ -1,5 +1,5 @@ // ERROR:8:self -unit badself; +unit badselfw; struct Box { value: i32, @@ -8,3 +8,5 @@ struct Box { return &self.value; } } + + diff --git a/fec/tests/own/badshwr.fe b/fec/tests/own/badshw1.fe similarity index 90% rename from fec/tests/own/badshwr.fe rename to fec/tests/own/badshw1.fe index 015d14f..c1c2d11 100644 --- a/fec/tests/own/badshwr.fe +++ b/fec/tests/own/badshw1.fe @@ -1,5 +1,5 @@ // ERROR:9:borrow -unit badshwr; +unit badshw1; fn read(r: &i32) -> i32 { return r.^; } @@ -9,3 +9,5 @@ fn bad() -> i32 { x = 1; return read(r); } + + diff --git a/fec/tests/own/badtwo.fe b/fec/tests/own/badtwo2.fe similarity index 82% rename from fec/tests/own/badtwo.fe rename to fec/tests/own/badtwo2.fe index dcdfa0e..431f7e9 100644 --- a/fec/tests/own/badtwo.fe +++ b/fec/tests/own/badtwo2.fe @@ -1,6 +1,8 @@ // ERROR:5:reference -unit badtwo; +unit badtwo2; fn choose(a: &i32, b: &i32) -> &i32 { return a; } + + diff --git a/fec/tests/own/badup.fe b/fec/tests/own/badup1.fe similarity index 89% rename from fec/tests/own/badup.fe rename to fec/tests/own/badup1.fe index 34f2b7e..a7b363c 100644 --- a/fec/tests/own/badup.fe +++ b/fec/tests/own/badup1.fe @@ -1,5 +1,5 @@ // ERROR:8:mut -unit badup; +unit badup1; struct Box { value: i32, @@ -8,3 +8,5 @@ struct Box { return &mut self.value; } } + + diff --git a/fec/tests/own/badweak.fe b/fec/tests/own/badweak2.fe similarity index 87% rename from fec/tests/own/badweak.fe rename to fec/tests/own/badweak2.fe index 8da6bc8..645e3fa 100644 --- a/fec/tests/own/badweak.fe +++ b/fec/tests/own/badweak2.fe @@ -1,5 +1,5 @@ // ERROR:7:mut -unit badweak; +unit badweak2; fn bad() -> void { var x: i32 = 0; @@ -7,3 +7,5 @@ fn bad() -> void { let s: &i32 = m; let v = s.^; } + + diff --git a/fec/tests/own/okbranch.fe b/fec/tests/own/okbrch.fe similarity index 93% rename from fec/tests/own/okbranch.fe rename to fec/tests/own/okbrch.fe index f6964b8..e68873e 100644 --- a/fec/tests/own/okbranch.fe +++ b/fec/tests/own/okbrch.fe @@ -1,4 +1,4 @@ -unit okbranch; +unit okbrch; fn read(r: &i32) -> i32 { return r.^; } @@ -13,3 +13,5 @@ fn test(cond: bool) -> i32 { x += 1; return x; } + + diff --git a/fec/tests/own/ok_defer.fe b/fec/tests/own/okdefer1.fe similarity index 80% rename from fec/tests/own/ok_defer.fe rename to fec/tests/own/okdefer1.fe index e4e2fef..518a806 100644 --- a/fec/tests/own/ok_defer.fe +++ b/fec/tests/own/okdefer1.fe @@ -1,5 +1,6 @@ -unit ok_defer; +unit okdefer1; pub fn cleanup(p: ^i32) -> void { defer { mem.destroy(p); } } + diff --git a/fec/tests/own/okdefer.fe b/fec/tests/own/okdefer2.fe similarity index 92% rename from fec/tests/own/okdefer.fe rename to fec/tests/own/okdefer2.fe index 1556b46..5185a16 100644 --- a/fec/tests/own/okdefer.fe +++ b/fec/tests/own/okdefer2.fe @@ -1,4 +1,4 @@ -unit okdefer; +unit okdefer2; fn read(r: &i32) -> i32 { return r.^; } @@ -11,3 +11,4 @@ fn test() -> i32 { x += 1; return x; } + diff --git a/fec/tests/own/okglobcp.fe b/fec/tests/own/okglobc.fe similarity index 89% rename from fec/tests/own/okglobcp.fe rename to fec/tests/own/okglobc.fe index e0c3e95..6760865 100644 --- a/fec/tests/own/okglobcp.fe +++ b/fec/tests/own/okglobc.fe @@ -1,4 +1,4 @@ -unit okglobcp; +unit okglobc; var VALUE: i32 = 7; @@ -8,3 +8,4 @@ fn test() -> i32 { let local = VALUE; return read(&local); } + diff --git a/fec/tests/own/oklast.fe b/fec/tests/own/oklast1.fe similarity index 86% rename from fec/tests/own/oklast.fe rename to fec/tests/own/oklast1.fe index 2e1e9f1..dfb7897 100644 --- a/fec/tests/own/oklast.fe +++ b/fec/tests/own/oklast1.fe @@ -1,4 +1,4 @@ -unit oklast; +unit oklast1; fn test() -> i32 { var x: i32 = 0; @@ -7,3 +7,5 @@ fn test() -> i32 { x += 1; return x; } + + diff --git a/fec/tests/own/ok_owned.fe b/fec/tests/own/okowned.fe similarity index 91% rename from fec/tests/own/ok_owned.fe rename to fec/tests/own/okowned.fe index 2881628..96581e5 100644 --- a/fec/tests/own/ok_owned.fe +++ b/fec/tests/own/okowned.fe @@ -1,4 +1,4 @@ -unit ok_owned; +unit okowned; fn main() -> !void { var p: ^i32 = try mem.create(0); @@ -7,3 +7,4 @@ fn main() -> !void { let value: i32 = p.^; defer { mem.destroy(p); } } + diff --git a/fec/tests/own/okr8free.fe b/fec/tests/own/okr8fr.fe similarity index 92% rename from fec/tests/own/okr8free.fe rename to fec/tests/own/okr8fr.fe index 0ae189e..49c2ac8 100644 --- a/fec/tests/own/okr8free.fe +++ b/fec/tests/own/okr8fr.fe @@ -1,4 +1,4 @@ -unit okr8free; +unit okr8fr; fn head(s: []u8) -> &u8 { return &s[0]; @@ -11,3 +11,5 @@ fn test() -> u8 { a[0] = 7 as u8; return v; } + + diff --git a/fec/tests/own/okr8join.fe b/fec/tests/own/okr8jn.fe similarity index 94% rename from fec/tests/own/okr8join.fe rename to fec/tests/own/okr8jn.fe index b2e67dd..9395a1e 100644 --- a/fec/tests/own/okr8join.fe +++ b/fec/tests/own/okr8jn.fe @@ -1,4 +1,4 @@ -unit okr8join; +unit okr8jn; fn select(s: str, from_param: bool) -> str { if from_param { return s; } @@ -12,3 +12,5 @@ fn test() -> u8 { bytes[0] = 8 as u8; return value; } + + diff --git a/fec/tests/own/okr8meth.fe b/fec/tests/own/okr8mt.fe similarity index 93% rename from fec/tests/own/okr8meth.fe rename to fec/tests/own/okr8mt.fe index 8f49258..271c723 100644 --- a/fec/tests/own/okr8meth.fe +++ b/fec/tests/own/okr8mt.fe @@ -1,4 +1,4 @@ -unit okr8meth; +unit okr8mt; struct Box { value: i32, @@ -15,3 +15,5 @@ fn test() -> i32 { b.value = 5; return v; } + + diff --git a/fec/tests/own/okr8stat.fe b/fec/tests/own/okr8st.fe similarity index 73% rename from fec/tests/own/okr8stat.fe rename to fec/tests/own/okr8st.fe index 5db67d4..6158944 100644 --- a/fec/tests/own/okr8stat.fe +++ b/fec/tests/own/okr8st.fe @@ -1,5 +1,7 @@ -unit okr8stat; +unit okr8st; fn name() -> str { return "main"; } + + diff --git a/fec/tests/own/okrebor.fe b/fec/tests/own/okrbor1.fe similarity index 90% rename from fec/tests/own/okrebor.fe rename to fec/tests/own/okrbor1.fe index b92ebf8..0569f27 100644 --- a/fec/tests/own/okrebor.fe +++ b/fec/tests/own/okrbor1.fe @@ -1,4 +1,4 @@ -unit okrebor; +unit okrbor1; fn read(r: &i32) -> i32 { return r.^; } @@ -9,3 +9,5 @@ fn test() -> i32 { r.^ = v + 1; return r.^; } + + diff --git a/fec/tests/own/okrtlast.fe b/fec/tests/own/okrtls.fe similarity index 91% rename from fec/tests/own/okrtlast.fe rename to fec/tests/own/okrtls.fe index bd87c4e..e917db9 100644 --- a/fec/tests/own/okrtlast.fe +++ b/fec/tests/own/okrtls.fe @@ -1,4 +1,4 @@ -unit okrtlast; +unit okrtls; struct Pair { a: i32, b: i32, } @@ -9,3 +9,5 @@ fn test() -> i32 { p.b = 4; return p.a + p.b; } + + diff --git a/fec/tests/own/okshare.fe b/fec/tests/own/okshar1.fe similarity index 90% rename from fec/tests/own/okshare.fe rename to fec/tests/own/okshar1.fe index 1740394..a82bdd0 100644 --- a/fec/tests/own/okshare.fe +++ b/fec/tests/own/okshar1.fe @@ -1,4 +1,4 @@ -unit okshare; +unit okshar1; fn add(a: &i32, b: &i32) -> i32 { return a.^ + b.^; @@ -10,3 +10,5 @@ fn test() -> i32 { let b = &x; return add(a, b); } + + diff --git a/fec/tests/own/okslreb.fe b/fec/tests/own/okslre1.fe similarity index 91% rename from fec/tests/own/okslreb.fe rename to fec/tests/own/okslre1.fe index 7b8a6a7..1f3aef6 100644 --- a/fec/tests/own/okslreb.fe +++ b/fec/tests/own/okslre1.fe @@ -1,4 +1,4 @@ -unit okslreb; +unit okslre1; fn first(s: []u8) -> u8 { return s[0]; } @@ -9,3 +9,5 @@ fn test() -> u8 { s[0] = 9 as u8; return v; } + + diff --git a/fec/tests/own/okstatic.fe b/fec/tests/own/okstat1.fe similarity index 86% rename from fec/tests/own/okstatic.fe rename to fec/tests/own/okstat1.fe index ae2487e..e231e3c 100644 --- a/fec/tests/own/okstatic.fe +++ b/fec/tests/own/okstat1.fe @@ -1,4 +1,4 @@ -unit okstatic; +unit okstat1; static VALUE: i32 = 7; @@ -9,3 +9,5 @@ fn get() -> &i32 { fn test() -> i32 { return get().^; } + + diff --git a/fec/tests/own/oktemp.fe b/fec/tests/own/oktemp1.fe similarity index 89% rename from fec/tests/own/oktemp.fe rename to fec/tests/own/oktemp1.fe index d88b1f7..c382b89 100644 --- a/fec/tests/own/oktemp.fe +++ b/fec/tests/own/oktemp1.fe @@ -1,4 +1,4 @@ -unit oktemp; +unit oktemp1; fn read(r: &i32) -> i32 { return r.^; } @@ -8,3 +8,5 @@ fn test() -> i32 { x += 1; return v + x; } + + diff --git a/fec/tests/own/oktrim.fe b/fec/tests/own/oktrim1.fe similarity index 78% rename from fec/tests/own/oktrim.fe rename to fec/tests/own/oktrim1.fe index cf416ac..f1e403c 100644 --- a/fec/tests/own/oktrim.fe +++ b/fec/tests/own/oktrim1.fe @@ -1,5 +1,7 @@ -unit oktrim; +unit oktrim1; fn trimmed(line: str) -> str { return line.trim(); } + + diff --git a/fec/tests/own/okwcall.fe b/fec/tests/own/okwcal1.fe similarity index 91% rename from fec/tests/own/okwcall.fe rename to fec/tests/own/okwcal1.fe index a53efe2..a81f447 100644 --- a/fec/tests/own/okwcall.fe +++ b/fec/tests/own/okwcal1.fe @@ -1,4 +1,4 @@ -unit okwcall; +unit okwcal1; fn read(r: &i32) -> i32 { return r.^; } @@ -9,3 +9,5 @@ fn test() -> i32 { m.^ = value + 1; return m.^; } + + diff --git a/fec/tests/types/bad_ari.fe b/fec/tests/types/badarith.fe similarity index 85% rename from fec/tests/types/bad_ari.fe rename to fec/tests/types/badarith.fe index bdf4619..424e3cf 100644 --- a/fec/tests/types/bad_ari.fe +++ b/fec/tests/types/badarith.fe @@ -1,4 +1,4 @@ -unit bad_ari; +unit badarith; fn add(a: i32, b: i32) -> i32 { return a + b; @@ -7,3 +7,4 @@ fn add(a: i32, b: i32) -> i32 { fn main() -> i32 { return add(1); } + diff --git a/fec/tests/types/badarr.fe b/fec/tests/types/badarry.fe similarity index 82% rename from fec/tests/types/badarr.fe rename to fec/tests/types/badarry.fe index 11007cb..ad4072f 100644 --- a/fec/tests/types/badarr.fe +++ b/fec/tests/types/badarry.fe @@ -1,5 +1,6 @@ -unit badarr; +unit badarry; fn main() -> i32 { let a: [2]i32 = [1, true, 3]; return a[0]; } + diff --git a/fec/tests/types/bad_asgn.fe b/fec/tests/types/badasgn.fe similarity index 84% rename from fec/tests/types/bad_asgn.fe rename to fec/tests/types/badasgn.fe index 3e921bc..eae96b2 100644 --- a/fec/tests/types/bad_asgn.fe +++ b/fec/tests/types/badasgn.fe @@ -1,7 +1,8 @@ -unit bad_asgn; +unit badasgn; fn main() -> i32 { let value: i32 = 1; value = 2; return value; } + diff --git a/fec/tests/types/badchar.fe b/fec/tests/types/badbyte.fe similarity index 79% rename from fec/tests/types/badchar.fe rename to fec/tests/types/badbyte.fe index 9d1a1d4..de2a923 100644 --- a/fec/tests/types/badchar.fe +++ b/fec/tests/types/badbyte.fe @@ -1,6 +1,7 @@ -unit badchar; +unit badbyte; fn main() -> i32 { let u: u8 = 'A'; return u; } + diff --git a/fec/tests/types/bad_cast.fe b/fec/tests/types/badcast.fe similarity index 81% rename from fec/tests/types/bad_cast.fe rename to fec/tests/types/badcast.fe index 0eafcfc..42ec79f 100644 --- a/fec/tests/types/bad_cast.fe +++ b/fec/tests/types/badcast.fe @@ -1,6 +1,7 @@ -unit bad_cast; +unit badcast; fn main() -> i32 { let x: i32 = true as i32; return x; } + diff --git a/fec/tests/types/bad_cond.fe b/fec/tests/types/badcond.fe similarity index 79% rename from fec/tests/types/bad_cond.fe rename to fec/tests/types/badcond.fe index a3968f1..5773959 100644 --- a/fec/tests/types/bad_cond.fe +++ b/fec/tests/types/badcond.fe @@ -1,6 +1,7 @@ -unit bad_cond; +unit badcond; fn main() -> i32 { if 1 { return 0; } return 1; } + diff --git a/fec/tests/types/badcycle.fe b/fec/tests/types/badcyc.fe similarity index 82% rename from fec/tests/types/badcycle.fe rename to fec/tests/types/badcyc.fe index 532ea1e..1b79348 100644 --- a/fec/tests/types/badcycle.fe +++ b/fec/tests/types/badcyc.fe @@ -1,6 +1,8 @@ -unit badcycle; +unit badcyc; struct A { b: B, } struct B { a: A, } fn main() -> i32 { return 0; } + + diff --git a/fec/tests/types/badfield.fe b/fec/tests/types/badfiel.fe similarity index 89% rename from fec/tests/types/badfield.fe rename to fec/tests/types/badfiel.fe index 4fa664c..6c0d2fd 100644 --- a/fec/tests/types/badfield.fe +++ b/fec/tests/types/badfiel.fe @@ -1,4 +1,4 @@ -unit badfield; +unit badfiel; struct Point { x: i32, y: i32, } fn main() -> i32 { @@ -6,3 +6,4 @@ fn main() -> i32 { p.x = 3; return p.x; } + diff --git a/fec/tests/types/badfld.fe b/fec/tests/types/badfmem.fe similarity index 87% rename from fec/tests/types/badfld.fe rename to fec/tests/types/badfmem.fe index 68704be..8814d35 100644 --- a/fec/tests/types/badfld.fe +++ b/fec/tests/types/badfmem.fe @@ -1,6 +1,7 @@ -unit badfld; +unit badfmem; struct Point { x: i32, y: i32, } fn main() -> i32 { let p: Point = Point{ x: 1 }; return p.z; } + diff --git a/fec/tests/types/badindex.fe b/fec/tests/types/badidx.fe similarity index 84% rename from fec/tests/types/badindex.fe rename to fec/tests/types/badidx.fe index 06b1b10..19c72f7 100644 --- a/fec/tests/types/badindex.fe +++ b/fec/tests/types/badidx.fe @@ -1,7 +1,8 @@ -unit badindex; +unit badidx; fn main() -> i32 { let a: [2]i32 = [1, 2]; a[0] = 3; return a[0]; } + diff --git a/fec/tests/types/badmat.fe b/fec/tests/types/badmatch.fe similarity index 87% rename from fec/tests/types/badmat.fe rename to fec/tests/types/badmatch.fe index 3c7df6a..0132224 100644 --- a/fec/tests/types/badmat.fe +++ b/fec/tests/types/badmatch.fe @@ -1,6 +1,7 @@ -unit badmat; +unit badmatch; enum Shape { Empty, Circle(i32), } fn main() -> i32 { match Shape.Empty { Empty => 0; } return 0; } + diff --git a/fec/tests/types/bad_mlet.fe b/fec/tests/types/badmlet.fe similarity index 84% rename from fec/tests/types/bad_mlet.fe rename to fec/tests/types/badmlet.fe index a3960b0..7283053 100644 --- a/fec/tests/types/bad_mlet.fe +++ b/fec/tests/types/badmlet.fe @@ -1,6 +1,7 @@ -unit bad_mlet; +unit badmlet; fn bad() -> void { var raw: [2]u8 = [1, 2]; let s: []mut u8 = raw[..]; } + diff --git a/fec/tests/types/bad_ret.fe b/fec/tests/types/badretu.fe similarity index 72% rename from fec/tests/types/bad_ret.fe rename to fec/tests/types/badretu.fe index a151ab3..bdeb958 100644 --- a/fec/tests/types/bad_ret.fe +++ b/fec/tests/types/badretu.fe @@ -1,5 +1,6 @@ -unit bad_ret; +unit badretu; fn main() -> i32 { return true; } + diff --git a/fec/tests/types/bad_shwr.fe b/fec/tests/types/badshwr.fe similarity index 74% rename from fec/tests/types/bad_shwr.fe rename to fec/tests/types/badshwr.fe index 7c7881f..d46977b 100644 --- a/fec/tests/types/bad_shwr.fe +++ b/fec/tests/types/badshwr.fe @@ -1,5 +1,6 @@ -unit bad_shwr; +unit badshwr; fn bad(s: []u8) -> void { s[0] = 1; } + diff --git a/fec/tests/types/badstr.fe b/fec/tests/types/badstrg.fe similarity index 84% rename from fec/tests/types/badstr.fe rename to fec/tests/types/badstrg.fe index 6219d9a..30f39e0 100644 --- a/fec/tests/types/badstr.fe +++ b/fec/tests/types/badstrg.fe @@ -1,6 +1,7 @@ -unit badstr; +unit badstrg; fn main() -> i32 { var text: str = "abc"; text[0] = 'z'; return 0; } + diff --git a/fec/tests/types/bad_type.fe b/fec/tests/types/badtype.fe similarity index 86% rename from fec/tests/types/bad_type.fe rename to fec/tests/types/badtype.fe index 15a4aca..c3fc863 100644 --- a/fec/tests/types/bad_type.fe +++ b/fec/tests/types/badtype.fe @@ -1,4 +1,4 @@ -unit bad_type; +unit badtype; fn add(a: i32, b: i32) -> i32 { return a + b; @@ -7,3 +7,4 @@ fn add(a: i32, b: i32) -> i32 { fn main() -> i32 { return add(true, 1); } + diff --git a/fec/tests/types/bad_unit.fe b/fec/tests/types/badunit.fe similarity index 78% rename from fec/tests/types/bad_unit.fe rename to fec/tests/types/badunit.fe index 99d8bbd..7074a19 100644 --- a/fec/tests/types/bad_unit.fe +++ b/fec/tests/types/badunit.fe @@ -1,6 +1,8 @@ -unit bad_unit; +unit badunit; fn main() -> i32 { var value: i32; return value; } + + diff --git a/fec/tests/types/bad_unk.fe b/fec/tests/types/badunk.fe similarity index 77% rename from fec/tests/types/bad_unk.fe rename to fec/tests/types/badunk.fe index b868c3b..ec28188 100644 --- a/fec/tests/types/bad_unk.fe +++ b/fec/tests/types/badunk.fe @@ -1,5 +1,6 @@ -unit bad_unk; +unit badunk; fn main() -> i32 { return missing_name; } + diff --git a/fec/tests/types/bad_void.fe b/fec/tests/types/badvoid.fe similarity index 86% rename from fec/tests/types/bad_void.fe rename to fec/tests/types/badvoid.fe index bf3ce1f..9ab0cae 100644 --- a/fec/tests/types/bad_void.fe +++ b/fec/tests/types/badvoid.fe @@ -1,4 +1,4 @@ -unit bad_void; +unit badvoid; fn noop() { return; @@ -8,3 +8,4 @@ fn main() -> i32 { let value: i32 = noop(); return value; } + diff --git a/fec/tests/types/ok_arra1.fe b/fec/tests/types/okarra1.fe similarity index 89% rename from fec/tests/types/ok_arra1.fe rename to fec/tests/types/okarra1.fe index 487b1ec..8df5963 100644 --- a/fec/tests/types/ok_arra1.fe +++ b/fec/tests/types/okarra1.fe @@ -1,7 +1,8 @@ -unit ok_arra1; +unit okarra1; fn main() -> i32 { let bytes: [3]u8 = [1, 2, 3]; if bytes[0] == 1 and bytes[2] == 3 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_array.fe b/fec/tests/types/okarray.fe similarity index 92% rename from fec/tests/types/ok_array.fe rename to fec/tests/types/okarray.fe index bf748bc..e11ef29 100644 --- a/fec/tests/types/ok_array.fe +++ b/fec/tests/types/okarray.fe @@ -1,4 +1,4 @@ -unit ok_array; +unit okarray; fn main() -> i32 { let a: [3]i32 = [1, 2, 3]; @@ -7,3 +7,4 @@ fn main() -> i32 { if a[0] + s[1] + t[0] == 5 and s.n == 3 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_castw.fe b/fec/tests/types/okcastw.fe similarity index 91% rename from fec/tests/types/ok_castw.fe rename to fec/tests/types/okcastw.fe index 2e962f5..5d3352e 100644 --- a/fec/tests/types/ok_castw.fe +++ b/fec/tests/types/okcastw.fe @@ -1,4 +1,4 @@ -unit ok_castw; +unit okcastw; pub fn main() -> i32 { var x: i16 = 0; @@ -9,3 +9,4 @@ pub fn main() -> i32 { if y == 3 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_char.fe b/fec/tests/types/okcharc.fe similarity index 91% rename from fec/tests/types/ok_char.fe rename to fec/tests/types/okcharc.fe index e6939b7..cd47d6b 100644 --- a/fec/tests/types/ok_char.fe +++ b/fec/tests/types/okcharc.fe @@ -1,4 +1,4 @@ -unit ok_char; +unit okcharc; fn main() -> i32 { let c: char = '\u0041'; @@ -7,3 +7,4 @@ fn main() -> i32 { if c == d and u == ('A' as u8) { return 0; } return 1; } + diff --git a/fec/tests/types/ok_enum.fe b/fec/tests/types/okenum.fe similarity index 96% rename from fec/tests/types/ok_enum.fe rename to fec/tests/types/okenum.fe index 581cbb3..8e6dfe4 100644 --- a/fec/tests/types/ok_enum.fe +++ b/fec/tests/types/okenum.fe @@ -1,4 +1,4 @@ -unit ok_enum; +unit okenum; enum Shape { Empty, Circle(i32), Rect { w: i32, h: i32, }, } @@ -14,3 +14,4 @@ fn main() -> i32 { if score(Shape.Circle(5)) == 5 and score(Shape.Rect{ w: 2, h: 3 }) == 6 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_for.fe b/fec/tests/types/okfor.fe similarity index 97% rename from fec/tests/types/ok_for.fe rename to fec/tests/types/okfor.fe index 3aab7bf..d191bda 100644 --- a/fec/tests/types/ok_for.fe +++ b/fec/tests/types/okfor.fe @@ -1,4 +1,4 @@ -unit ok_for; +unit okfor; fn main() -> i32 { var total: i32 = 0; @@ -16,3 +16,4 @@ fn main() -> i32 { if total == 9 and m[0] == 2 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_hello.fe b/fec/tests/types/okhello.fe similarity index 95% rename from fec/tests/types/ok_hello.fe rename to fec/tests/types/okhello.fe index e55fedb..ed5305b 100644 --- a/fec/tests/types/ok_hello.fe +++ b/fec/tests/types/okhello.fe @@ -1,4 +1,4 @@ -unit ok_hello; +unit okhello; fn add(a: i32, b: i32) -> i32 { return a + b; @@ -21,3 +21,4 @@ pub fn main() -> i32 { return 1; } } + diff --git a/fec/tests/types/ok_mutab.fe b/fec/tests/types/okmutab.fe similarity index 93% rename from fec/tests/types/ok_mutab.fe rename to fec/tests/types/okmutab.fe index dc596a2..8b44f6c 100644 --- a/fec/tests/types/ok_mutab.fe +++ b/fec/tests/types/okmutab.fe @@ -1,4 +1,4 @@ -unit ok_mutab; +unit okmutab; fn takes_shared(s: []u8) -> u8 { return s[0]; } @@ -9,3 +9,4 @@ fn main() -> i32 { if takes_shared(s) == 1 and raw[1] == 9 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_neste.fe b/fec/tests/types/okneste.fe similarity index 92% rename from fec/tests/types/ok_neste.fe rename to fec/tests/types/okneste.fe index e8c0485..cf9c392 100644 --- a/fec/tests/types/ok_neste.fe +++ b/fec/tests/types/okneste.fe @@ -1,4 +1,4 @@ -unit ok_neste; +unit okneste; struct Outer { inner: Inner, } struct Inner { value: i32, } @@ -8,3 +8,4 @@ fn main() -> i32 { if x.inner.value == 7 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_scope.fe b/fec/tests/types/okscope.fe similarity index 94% rename from fec/tests/types/ok_scope.fe rename to fec/tests/types/okscope.fe index 153398f..187a837 100644 --- a/fec/tests/types/ok_scope.fe +++ b/fec/tests/types/okscope.fe @@ -1,4 +1,4 @@ -unit ok_scope; +unit okscope; fn register(switch: i32) -> i32 { let auto: i32 = switch; @@ -13,3 +13,4 @@ fn register(switch: i32) -> i32 { pub fn main() -> i32 { return register(1); } + diff --git a/fec/tests/types/ok_str.fe b/fec/tests/types/okstrg.fe similarity index 89% rename from fec/tests/types/ok_str.fe rename to fec/tests/types/okstrg.fe index fec3064..c758382 100644 --- a/fec/tests/types/ok_str.fe +++ b/fec/tests/types/okstrg.fe @@ -1,7 +1,8 @@ -unit ok_str; +unit okstrg; fn main() -> i32 { let text: str = "abc"; if text[1] == ('b' as u8) and text.n == 3 { return 0; } return 1; } + diff --git a/fec/tests/types/ok_struc.fe b/fec/tests/types/okstruc.fe similarity index 96% rename from fec/tests/types/ok_struc.fe rename to fec/tests/types/okstruc.fe index 1754eb9..ac73d35 100644 --- a/fec/tests/types/ok_struc.fe +++ b/fec/tests/types/okstruc.fe @@ -1,4 +1,4 @@ -unit ok_struc; +unit okstruc; struct Point { x: i32, y: i32, } packed struct PackedPoint { x: u8, y: i32, } @@ -12,3 +12,4 @@ fn main() -> i32 { (Point{ x: 1, y: 2 }.x == 1) and @size_of(Natural) == 8 { return 0; } return 1; } + From 79aa2208f10e768594d8a7a4243cb18e13aab98f Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:14:35 +0900 Subject: [PATCH 119/184] =?UTF-8?q?tests:=20pending-backend=20README=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fec/tests/pending-backend/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 fec/tests/pending-backend/README.md diff --git a/fec/tests/pending-backend/README.md b/fec/tests/pending-backend/README.md new file mode 100644 index 0000000..a653444 --- /dev/null +++ b/fec/tests/pending-backend/README.md @@ -0,0 +1,10 @@ +`fec/tests/pending-backend/`에 남겨둔 fixture는 현재 프론트엔드 전용 상태에서 실행할 수 없습니다. + +- `bounds_trap.fe` / `bounds_nocheck.fe`: 경계 검사 실패가 실제로 trap되는지, + `--no-checks` 플래그가 그 검사를 제거하는지 확인하는 런타임 동작 테스트입니다. +- `ownership_drop.fe` + `ownership-drop.c`: 삽입된 `drop`/`defer`가 실제로 실행되는지 확인하는 테스트입니다. + `ownership-drop.c`는 해제 횟수/순서/이중 해제를 검증하는 하네스입니다. +- `format-prop.c`: 포맷 프로퍼티 동작을 확인하는 런타임 검사입니다. + +이들은 코드 생성기가 없는 현재 단계에서는 실행할 수 없어서 `tests/run.py`가 건너뜁니다. +백엔드(코드 생성기)가 돌아오면 가장 먼저 재활성화할 대상입니다. From 43646647afd2ec87e9f06d1c9ffe021959e8d93f Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:14:37 +0900 Subject: [PATCH 120/184] =?UTF-8?q?docs:=20AGENTS=EB=A5=BC=20=ED=94=84?= =?UTF-8?q?=EB=9F=B0=ED=8A=B8=EC=97=94=EB=93=9C=20=EC=A0=84=EC=9A=A9=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A1=9C=20=EA=B0=B1=EC=8B=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9f7b7af..7da3312 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,38 +10,39 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 | `SPEC.md` | 언어 명세. 유일한 규범 문서. 구현 지시서와 표준 라이브러리 명세는 별도 문서 | | `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | -개발 환경과 테스트 명령 목록·플래그는 CLI로 확인한다. +개발 환경과 테스트 명령 목록·플래그는 다음 CLI로 확인한다. ```powershell -uv run ferro-dos --help -uv run ferro-dos --help -uv run ferro-test --help +uv run python tests/run.py --help ``` ## 검증 규칙 -- 컴파일러와 생성 C는 DOSBox-X 내부의 고정된 Open Watcom으로 컴파일한다. - 컴파일러와 bits16은 `WCL`, bits32 생성 C는 `WCL386`을 쓴다. -- 호스트 C 컴파일러 결과는 검증으로 인정하지 않는다. 호스트는 편집, Git, 다운로드, - 격리 작업공간 준비에만 쓴다. -- 실행마다 만들어지는 authoritative workspace는 `C:\FEC`다. 호스트에서는 - `.dosboxx/runs//FEC`에 대응한다. -- 완료하려는 기능을 직접 검사하는 pytest case가 통과해야 한다. 테스트가 증명하지 +- 컴파일러는 현재 프런트엔드만 구현되어 있다. 범위: lexer/parser/types/own/check/resolve. +- 코드 생성기(백엔드/IR/`lowering`)는 아직 없다. +- 호스트 C 컴파일러는 구현/검증 대상이 아니다. 호스트는 편집, Git, 다운로드, 격리 + 작업공간 준비에만 쓴다. +- 완료하려는 기능을 직접 검사하는 `pytest` case가 통과해야 한다. 테스트가 증명하지 않는 기능은 완료로 처리하지 않는다. +- `uv run python tests/run.py` 는 프런트엔드 검증 엔트리이다. - VGA 데모처럼 수동 검증이 필요한 항목은 자동 완료 게이트에서 제외한다. +- 실행 환경은 `.dosboxx/`와 `tests/run.py`로 준비되며, 과거 VM 이미지/호스트 바이너리는 + 완료 근거로 쓰지 않는다. ## 빌드 함정 -- 컴파일러는 16비트 large model로 빌드한다. small model은 메모리 부족으로 실패한다. -- 링크는 `*.obj` 와일드카드로 한다. DOS 명령줄 길이 제한 때문에 오브젝트를 나열할 - 수 없다. `fec/build-dos.bat`는 먼저 stale object를 지운다. -- M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다. 생성 C의 보수적 미사용 helper 때문에 - W202만 끄고 나머지 경고는 오류로 유지한다. -- fixture는 DOS 8.3 이름으로 실행한다. 긴 이름은 registry에서 명시적으로 줄인다. -- `R:`은 읽기 전용 저장소, `W:`은 읽기 전용 Watcom이다. 빌드 산출물은 반드시 - 임시 `C:\FEC`에 쓴다. -- 실패 분석이 필요하면 `ferro-test --keep-failed` 또는 `ferro-dos --keep`으로 - 임시 작업공간을 보존한다. +- [백엔드 복귀 시 유효] 컴파일러는 16비트 large model로 빌드한다. small model은 + 메모리 부족으로 실패한다. +- [백엔드 복귀 시 유효] 링크는 `*.obj` 와일드카드로 한다. DOS 명령줄 길이 제한 때문에 + 오브젝트를 개별 열거할 수 없다. +- [백엔드 복귀 시 유효] M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다. 생성 C의 보수적 미 + 사용 helper 때문에 W202만 끄고 나머지 경고는 오류로 유지한다. +- [백엔드 복귀 시 유효] fixture는 DOS 8.3 이름으로 실행한다. 긴 이름은 registry에서 + 명시적으로 줄인다. +- [백엔드 복귀 시 유효] `R:`은 읽기 전용 저장소, `W:`은 읽기 전용 Watcom이다. + 빌드 산출물은 반드시 임시 `C:\FEC`에 쓴다. +- [백엔드 복귀 시 유효] 실패 분석이 필요하면 `tests/run.py`의 `--keep-failed` 또는 + 런처 플래그로 임시 작업공간을 보존한다. ## 작업 흐름 From 7636a41a4eb898b7d10f327f8c103fae14e606b3 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:21:13 +0900 Subject: [PATCH 121/184] =?UTF-8?q?Revert=20"tests:=20fixture=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=AA=85=EA=B3=BC=20unit=20=EB=8F=99=EA=B8=B0?= =?UTF-8?q?=ED=99=94"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 밑줄만 제거하고 충돌한 이름에 숫자를 붙인 결과라 이름이 무엇을 검사하는지 오히려 덜 드러낸다. own/badfld 와 types/badfld 가 서로 다른 것을 검사하는데 둘 다 badfmem 이 된 것이 그 증거다. 파일을 열어 판단하는 작업이므로 마커 판정과 함께 다시 한다. --- fec/tests/format/{badfmta.fe => bad_ari.fe} | 3 +-- fec/tests/format/{badfbuf.fe => bad_bufw.fe} | 3 +-- fec/tests/format/{badfcls.fe => bad_cls.fe} | 3 +-- fec/tests/format/{badfmany.fe => bad_many.fe} | 3 +-- fec/tests/format/{badfopen.fe => bad_open.fe} | 3 +-- fec/tests/format/{badfrun.fe => bad_run.fe} | 3 +-- fec/tests/format/{badftry.fe => bad_try.fe} | 3 +-- fec/tests/format/{badftype.fe => bad_type.fe} | 3 +-- fec/tests/format/{badfverb.fe => bad_verb.fe} | 3 +-- fec/tests/format/{badfwrit.fe => bad_writ.fe} | 3 +-- fec/tests/format/{okfmt.fe => ok_forma.fe} | 3 +-- fec/tests/format/{okprop.fe => ok_prop.fe} | 3 +-- fec/tests/format/{oktryf.fe => ok_try_f.fe} | 3 +-- fec/tests/optional/{badcatc.fe => badcatch.fe} | 3 +-- fec/tests/optional/{baddefi.fe => baddef.fe} | 4 +--- fec/tests/optional/{badidir.fe => baddir.fe} | 3 +-- fec/tests/optional/{badecod.fe => badercod.fe} | 3 +-- fec/tests/optional/{badenam.fe => badernam.fe} | 3 +-- fec/tests/optional/{badetyp.fe => badetype.fe} | 4 +--- fec/tests/optional/{badnullv.fe => badnull.fe} | 4 +--- fec/tests/optional/{badorefv.fe => badoref.fe} | 4 +--- fec/tests/optional/{badorlx.fe => badorel.fe} | 4 +--- fec/tests/optional/{badprjv.fe => badproj.fe} | 4 +--- fec/tests/optional/{badqmkv.fe => badqmark.fe} | 4 +--- fec/tests/optional/{badrett.fe => badret.fe} | 3 +-- fec/tests/optional/{badsomev.fe => badsome.fe} | 4 +--- fec/tests/optional/{badtryx.fe => badtry.fe} | 4 +--- fec/tests/optional/{badzero1.fe => badzero.fe} | 4 +--- fec/tests/optional/{okcatc.fe => okcatch.fe} | 3 +-- fec/tests/optional/{okcatmv.fe => okcatmov.fe} | 3 +-- fec/tests/optional/{okcvd.fe => okcvoid.fe} | 3 +-- fec/tests/optional/{okdefl.fe => okdeflt.fe} | 3 +-- fec/tests/optional/{okiflt.fe => okiflet.fe} | 4 +--- fec/tests/optional/{okmtch.fe => okmatch.fe} | 4 +--- fec/tests/optional/{oknull1.fe => oknull.fe} | 4 +--- fec/tests/optional/{okorel.fe => okorelse.fe} | 3 +-- fec/tests/optional/{okptvw.fe => okpatvw.fe} | 4 +--- fec/tests/optional/{okproj1.fe => okproj.fe} | 4 +--- fec/tests/optional/{okrepl2.fe => okrepl.fe} | 4 +--- fec/tests/optional/{oktrd2.fe => oktrdef.fe} | 4 +--- fec/tests/optional/{oktry2.fe => oktry.fe} | 4 +--- fec/tests/own/{badclos.fe => bad_clos.fe} | 3 +-- fec/tests/own/{badcond.fe => bad_cond.fe} | 3 +-- fec/tests/own/{baddbl.fe => bad_dbl.fe} | 3 +-- fec/tests/own/{baddest.fe => bad_dest.fe} | 3 +-- fec/tests/own/{baddrop.fe => bad_drop.fe} | 3 +-- fec/tests/own/{badlop1.fe => bad_loop.fe} | 3 +-- fec/tests/own/{badmv1.fe => bad_move.fe} | 3 +-- fec/tests/own/{badproj.fe => bad_proj.fe} | 3 +-- fec/tests/own/{badargu.fe => badarg.fe} | 4 +--- fec/tests/own/{badinit.fe => badbinit.fe} | 3 +-- fec/tests/own/{badbrmv.fe => badbrmov.fe} | 3 +-- fec/tests/own/{baddefr.fe => baddefer.fe} | 3 +-- fec/tests/own/{badfmem.fe => badfld.fe} | 3 +-- fec/tests/own/{badglobx.fe => badglob.fe} | 4 +--- fec/tests/own/{badinvr.fe => badinv.fe} | 4 +--- fec/tests/own/{badlp2.fe => badloop.fe} | 3 +-- fec/tests/own/{badmv2.fe => badmove.fe} | 3 +-- fec/tests/own/{badmutv.fe => badmut.fe} | 4 +--- fec/tests/own/{badmutx.fe => badmut2.fe} | 4 +--- fec/tests/own/{badptrx.fe => badptr.fe} | 4 +--- fec/tests/own/{badretv.fe => badret.fe} | 3 +-- fec/tests/own/{badrfie.fe => badrfld.fe} | 4 +--- fec/tests/own/{badri2.fe => badridx.fe} | 4 +--- fec/tests/own/{badscp1.fe => badscop.fe} | 4 +--- fec/tests/own/{badselfw.fe => badself.fe} | 4 +--- fec/tests/own/{badshw1.fe => badshwr.fe} | 4 +--- fec/tests/own/{badtwo2.fe => badtwo.fe} | 4 +--- fec/tests/own/{badup1.fe => badup.fe} | 4 +--- fec/tests/own/{badweak2.fe => badweak.fe} | 4 +--- fec/tests/own/{okdefer1.fe => ok_defer.fe} | 3 +-- fec/tests/own/{okowned.fe => ok_owned.fe} | 3 +-- fec/tests/own/{okbrch.fe => okbranch.fe} | 4 +--- fec/tests/own/{okdefer2.fe => okdefer.fe} | 3 +-- fec/tests/own/{okglobc.fe => okglobcp.fe} | 3 +-- fec/tests/own/{oklast1.fe => oklast.fe} | 4 +--- fec/tests/own/{okr8fr.fe => okr8free.fe} | 4 +--- fec/tests/own/{okr8jn.fe => okr8join.fe} | 4 +--- fec/tests/own/{okr8mt.fe => okr8meth.fe} | 4 +--- fec/tests/own/{okr8st.fe => okr8stat.fe} | 4 +--- fec/tests/own/{okrbor1.fe => okrebor.fe} | 4 +--- fec/tests/own/{okrtls.fe => okrtlast.fe} | 4 +--- fec/tests/own/{okshar1.fe => okshare.fe} | 4 +--- fec/tests/own/{okslre1.fe => okslreb.fe} | 4 +--- fec/tests/own/{okstat1.fe => okstatic.fe} | 4 +--- fec/tests/own/{oktemp1.fe => oktemp.fe} | 4 +--- fec/tests/own/{oktrim1.fe => oktrim.fe} | 4 +--- fec/tests/own/{okwcal1.fe => okwcall.fe} | 4 +--- fec/tests/types/{badarith.fe => bad_ari.fe} | 3 +-- fec/tests/types/{badasgn.fe => bad_asgn.fe} | 3 +-- fec/tests/types/{badcast.fe => bad_cast.fe} | 3 +-- fec/tests/types/{badcond.fe => bad_cond.fe} | 3 +-- fec/tests/types/{badmlet.fe => bad_mlet.fe} | 3 +-- fec/tests/types/{badretu.fe => bad_ret.fe} | 3 +-- fec/tests/types/{badshwr.fe => bad_shwr.fe} | 3 +-- fec/tests/types/{badtype.fe => bad_type.fe} | 3 +-- fec/tests/types/{badunit.fe => bad_unit.fe} | 4 +--- fec/tests/types/{badunk.fe => bad_unk.fe} | 3 +-- fec/tests/types/{badvoid.fe => bad_void.fe} | 3 +-- fec/tests/types/{badarry.fe => badarr.fe} | 3 +-- fec/tests/types/{badbyte.fe => badchar.fe} | 3 +-- fec/tests/types/{badcyc.fe => badcycle.fe} | 4 +--- fec/tests/types/{badfiel.fe => badfield.fe} | 3 +-- fec/tests/types/{badfmem.fe => badfld.fe} | 3 +-- fec/tests/types/{badidx.fe => badindex.fe} | 3 +-- fec/tests/types/{badmatch.fe => badmat.fe} | 3 +-- fec/tests/types/{badstrg.fe => badstr.fe} | 3 +-- fec/tests/types/{okarra1.fe => ok_arra1.fe} | 3 +-- fec/tests/types/{okarray.fe => ok_array.fe} | 3 +-- fec/tests/types/{okcastw.fe => ok_castw.fe} | 3 +-- fec/tests/types/{okcharc.fe => ok_char.fe} | 3 +-- fec/tests/types/{okenum.fe => ok_enum.fe} | 3 +-- fec/tests/types/{okfor.fe => ok_for.fe} | 3 +-- fec/tests/types/{okhello.fe => ok_hello.fe} | 3 +-- fec/tests/types/{okmutab.fe => ok_mutab.fe} | 3 +-- fec/tests/types/{okneste.fe => ok_neste.fe} | 3 +-- fec/tests/types/{okscope.fe => ok_scope.fe} | 3 +-- fec/tests/types/{okstrg.fe => ok_str.fe} | 3 +-- fec/tests/types/{okstruc.fe => ok_struc.fe} | 3 +-- 119 files changed, 119 insertions(+), 286 deletions(-) rename fec/tests/format/{badfmta.fe => bad_ari.fe} (80%) rename fec/tests/format/{badfbuf.fe => bad_bufw.fe} (89%) rename fec/tests/format/{badfcls.fe => bad_cls.fe} (78%) rename fec/tests/format/{badfmany.fe => bad_many.fe} (78%) rename fec/tests/format/{badfopen.fe => bad_open.fe} (77%) rename fec/tests/format/{badfrun.fe => bad_run.fe} (84%) rename fec/tests/format/{badftry.fe => bad_try.fe} (80%) rename fec/tests/format/{badftype.fe => bad_type.fe} (87%) rename fec/tests/format/{badfverb.fe => bad_verb.fe} (78%) rename fec/tests/format/{badfwrit.fe => bad_writ.fe} (83%) rename fec/tests/format/{okfmt.fe => ok_forma.fe} (98%) rename fec/tests/format/{okprop.fe => ok_prop.fe} (84%) rename fec/tests/format/{oktryf.fe => ok_try_f.fe} (91%) rename fec/tests/optional/{badcatc.fe => badcatch.fe} (92%) rename fec/tests/optional/{baddefi.fe => baddef.fe} (89%) rename fec/tests/optional/{badidir.fe => baddir.fe} (87%) rename fec/tests/optional/{badecod.fe => badercod.fe} (80%) rename fec/tests/optional/{badenam.fe => badernam.fe} (80%) rename fec/tests/optional/{badetyp.fe => badetype.fe} (90%) rename fec/tests/optional/{badnullv.fe => badnull.fe} (76%) rename fec/tests/optional/{badorefv.fe => badoref.fe} (76%) rename fec/tests/optional/{badorlx.fe => badorel.fe} (89%) rename fec/tests/optional/{badprjv.fe => badproj.fe} (88%) rename fec/tests/optional/{badqmkv.fe => badqmark.fe} (82%) rename fec/tests/optional/{badrett.fe => badret.fe} (88%) rename fec/tests/optional/{badsomev.fe => badsome.fe} (80%) rename fec/tests/optional/{badtryx.fe => badtry.fe} (88%) rename fec/tests/optional/{badzero1.fe => badzero.fe} (70%) rename fec/tests/optional/{okcatc.fe => okcatch.fe} (92%) rename fec/tests/optional/{okcatmv.fe => okcatmov.fe} (90%) rename fec/tests/optional/{okcvd.fe => okcvoid.fe} (90%) rename fec/tests/optional/{okdefl.fe => okdeflt.fe} (89%) rename fec/tests/optional/{okiflt.fe => okiflet.fe} (86%) rename fec/tests/optional/{okmtch.fe => okmatch.fe} (88%) rename fec/tests/optional/{oknull1.fe => oknull.fe} (90%) rename fec/tests/optional/{okorel.fe => okorelse.fe} (77%) rename fec/tests/optional/{okptvw.fe => okpatvw.fe} (93%) rename fec/tests/optional/{okproj1.fe => okproj.fe} (84%) rename fec/tests/optional/{okrepl2.fe => okrepl.fe} (90%) rename fec/tests/optional/{oktrd2.fe => oktrdef.fe} (90%) rename fec/tests/optional/{oktry2.fe => oktry.fe} (90%) rename fec/tests/own/{badclos.fe => bad_clos.fe} (94%) rename fec/tests/own/{badcond.fe => bad_cond.fe} (89%) rename fec/tests/own/{baddbl.fe => bad_dbl.fe} (83%) rename fec/tests/own/{baddest.fe => bad_dest.fe} (76%) rename fec/tests/own/{baddrop.fe => bad_drop.fe} (90%) rename fec/tests/own/{badlop1.fe => bad_loop.fe} (88%) rename fec/tests/own/{badmv1.fe => bad_move.fe} (87%) rename fec/tests/own/{badproj.fe => bad_proj.fe} (88%) rename fec/tests/own/{badargu.fe => badarg.fe} (90%) rename fec/tests/own/{badinit.fe => badbinit.fe} (90%) rename fec/tests/own/{badbrmv.fe => badbrmov.fe} (89%) rename fec/tests/own/{baddefr.fe => baddefer.fe} (92%) rename fec/tests/own/{badfmem.fe => badfld.fe} (78%) rename fec/tests/own/{badglobx.fe => badglob.fe} (84%) rename fec/tests/own/{badinvr.fe => badinv.fe} (88%) rename fec/tests/own/{badlp2.fe => badloop.fe} (90%) rename fec/tests/own/{badmv2.fe => badmove.fe} (90%) rename fec/tests/own/{badmutv.fe => badmut.fe} (87%) rename fec/tests/own/{badmutx.fe => badmut2.fe} (89%) rename fec/tests/own/{badptrx.fe => badptr.fe} (79%) rename fec/tests/own/{badretv.fe => badret.fe} (83%) rename fec/tests/own/{badrfie.fe => badrfld.fe} (90%) rename fec/tests/own/{badri2.fe => badridx.fe} (90%) rename fec/tests/own/{badscp1.fe => badscop.fe} (92%) rename fec/tests/own/{badselfw.fe => badself.fe} (87%) rename fec/tests/own/{badshw1.fe => badshwr.fe} (90%) rename fec/tests/own/{badtwo2.fe => badtwo.fe} (82%) rename fec/tests/own/{badup1.fe => badup.fe} (89%) rename fec/tests/own/{badweak2.fe => badweak.fe} (87%) rename fec/tests/own/{okdefer1.fe => ok_defer.fe} (80%) rename fec/tests/own/{okowned.fe => ok_owned.fe} (91%) rename fec/tests/own/{okbrch.fe => okbranch.fe} (93%) rename fec/tests/own/{okdefer2.fe => okdefer.fe} (92%) rename fec/tests/own/{okglobc.fe => okglobcp.fe} (89%) rename fec/tests/own/{oklast1.fe => oklast.fe} (86%) rename fec/tests/own/{okr8fr.fe => okr8free.fe} (92%) rename fec/tests/own/{okr8jn.fe => okr8join.fe} (94%) rename fec/tests/own/{okr8mt.fe => okr8meth.fe} (93%) rename fec/tests/own/{okr8st.fe => okr8stat.fe} (73%) rename fec/tests/own/{okrbor1.fe => okrebor.fe} (90%) rename fec/tests/own/{okrtls.fe => okrtlast.fe} (91%) rename fec/tests/own/{okshar1.fe => okshare.fe} (90%) rename fec/tests/own/{okslre1.fe => okslreb.fe} (91%) rename fec/tests/own/{okstat1.fe => okstatic.fe} (86%) rename fec/tests/own/{oktemp1.fe => oktemp.fe} (89%) rename fec/tests/own/{oktrim1.fe => oktrim.fe} (78%) rename fec/tests/own/{okwcal1.fe => okwcall.fe} (91%) rename fec/tests/types/{badarith.fe => bad_ari.fe} (85%) rename fec/tests/types/{badasgn.fe => bad_asgn.fe} (84%) rename fec/tests/types/{badcast.fe => bad_cast.fe} (81%) rename fec/tests/types/{badcond.fe => bad_cond.fe} (79%) rename fec/tests/types/{badmlet.fe => bad_mlet.fe} (84%) rename fec/tests/types/{badretu.fe => bad_ret.fe} (72%) rename fec/tests/types/{badshwr.fe => bad_shwr.fe} (74%) rename fec/tests/types/{badtype.fe => bad_type.fe} (86%) rename fec/tests/types/{badunit.fe => bad_unit.fe} (78%) rename fec/tests/types/{badunk.fe => bad_unk.fe} (77%) rename fec/tests/types/{badvoid.fe => bad_void.fe} (86%) rename fec/tests/types/{badarry.fe => badarr.fe} (82%) rename fec/tests/types/{badbyte.fe => badchar.fe} (79%) rename fec/tests/types/{badcyc.fe => badcycle.fe} (82%) rename fec/tests/types/{badfiel.fe => badfield.fe} (89%) rename fec/tests/types/{badfmem.fe => badfld.fe} (87%) rename fec/tests/types/{badidx.fe => badindex.fe} (84%) rename fec/tests/types/{badmatch.fe => badmat.fe} (87%) rename fec/tests/types/{badstrg.fe => badstr.fe} (84%) rename fec/tests/types/{okarra1.fe => ok_arra1.fe} (89%) rename fec/tests/types/{okarray.fe => ok_array.fe} (92%) rename fec/tests/types/{okcastw.fe => ok_castw.fe} (91%) rename fec/tests/types/{okcharc.fe => ok_char.fe} (91%) rename fec/tests/types/{okenum.fe => ok_enum.fe} (96%) rename fec/tests/types/{okfor.fe => ok_for.fe} (97%) rename fec/tests/types/{okhello.fe => ok_hello.fe} (95%) rename fec/tests/types/{okmutab.fe => ok_mutab.fe} (93%) rename fec/tests/types/{okneste.fe => ok_neste.fe} (92%) rename fec/tests/types/{okscope.fe => ok_scope.fe} (94%) rename fec/tests/types/{okstrg.fe => ok_str.fe} (89%) rename fec/tests/types/{okstruc.fe => ok_struc.fe} (96%) diff --git a/fec/tests/format/badfmta.fe b/fec/tests/format/bad_ari.fe similarity index 80% rename from fec/tests/format/badfmta.fe rename to fec/tests/format/bad_ari.fe index 01534cb..7dc30b8 100644 --- a/fec/tests/format/badfmta.fe +++ b/fec/tests/format/bad_ari.fe @@ -1,7 +1,6 @@ -unit badfmta; +unit bad_ari; fn main() -> i32 { @print("{} {}", 1); return 0; } - diff --git a/fec/tests/format/badfbuf.fe b/fec/tests/format/bad_bufw.fe similarity index 89% rename from fec/tests/format/badfbuf.fe rename to fec/tests/format/bad_bufw.fe index 2267cb8..37bf521 100644 --- a/fec/tests/format/badfbuf.fe +++ b/fec/tests/format/bad_bufw.fe @@ -1,8 +1,7 @@ -unit badfbuf; +unit bad_bufw; fn main() -> void { var raw: [4]u8 = [0, 0, 0, 0]; var buf: []mut u8 = raw[..]; let w = io.buf_writer(buf); } - diff --git a/fec/tests/format/badfcls.fe b/fec/tests/format/bad_cls.fe similarity index 78% rename from fec/tests/format/badfcls.fe rename to fec/tests/format/bad_cls.fe index 032d923..1f8a720 100644 --- a/fec/tests/format/badfcls.fe +++ b/fec/tests/format/bad_cls.fe @@ -1,7 +1,6 @@ -unit badfcls; +unit bad_cls; fn main() -> i32 { @print("}", 1); return 0; } - diff --git a/fec/tests/format/badfmany.fe b/fec/tests/format/bad_many.fe similarity index 78% rename from fec/tests/format/badfmany.fe rename to fec/tests/format/bad_many.fe index 044e758..ab25401 100644 --- a/fec/tests/format/badfmany.fe +++ b/fec/tests/format/bad_many.fe @@ -1,7 +1,6 @@ -unit badfmany; +unit bad_many; fn main() -> i32 { @print("{}", 1, 2); return 0; } - diff --git a/fec/tests/format/badfopen.fe b/fec/tests/format/bad_open.fe similarity index 77% rename from fec/tests/format/badfopen.fe rename to fec/tests/format/bad_open.fe index 6b32d2b..98ee2c8 100644 --- a/fec/tests/format/badfopen.fe +++ b/fec/tests/format/bad_open.fe @@ -1,7 +1,6 @@ -unit badfopen; +unit bad_open; fn main() -> i32 { @print("{", 1); return 0; } - diff --git a/fec/tests/format/badfrun.fe b/fec/tests/format/bad_run.fe similarity index 84% rename from fec/tests/format/badfrun.fe rename to fec/tests/format/bad_run.fe index eed67b9..c7945e1 100644 --- a/fec/tests/format/badfrun.fe +++ b/fec/tests/format/bad_run.fe @@ -1,8 +1,7 @@ -unit badfrun; +unit bad_run; fn main() -> i32 { var fmt: str = "{}"; @print(fmt, 1); return 0; } - diff --git a/fec/tests/format/badftry.fe b/fec/tests/format/bad_try.fe similarity index 80% rename from fec/tests/format/badftry.fe rename to fec/tests/format/bad_try.fe index e3fb5f6..0c5da62 100644 --- a/fec/tests/format/badftry.fe +++ b/fec/tests/format/bad_try.fe @@ -1,7 +1,6 @@ -unit badftry; +unit bad_try; fn main() -> i32 { try @print("nope"); return 0; } - diff --git a/fec/tests/format/badftype.fe b/fec/tests/format/bad_type.fe similarity index 87% rename from fec/tests/format/badftype.fe rename to fec/tests/format/bad_type.fe index 72c4fbb..9279e47 100644 --- a/fec/tests/format/badftype.fe +++ b/fec/tests/format/bad_type.fe @@ -1,4 +1,4 @@ -unit badftype; +unit bad_type; struct Point { x: i32, } @@ -7,4 +7,3 @@ fn main() -> i32 { @print("{}", p); return 0; } - diff --git a/fec/tests/format/badfverb.fe b/fec/tests/format/bad_verb.fe similarity index 78% rename from fec/tests/format/badfverb.fe rename to fec/tests/format/bad_verb.fe index 51e5308..9a928ce 100644 --- a/fec/tests/format/badfverb.fe +++ b/fec/tests/format/bad_verb.fe @@ -1,7 +1,6 @@ -unit badfverb; +unit bad_verb; fn main() -> i32 { @print("{q}", 1); return 0; } - diff --git a/fec/tests/format/badfwrit.fe b/fec/tests/format/bad_writ.fe similarity index 83% rename from fec/tests/format/badfwrit.fe rename to fec/tests/format/bad_writ.fe index f5f84a9..18e33a6 100644 --- a/fec/tests/format/badfwrit.fe +++ b/fec/tests/format/bad_writ.fe @@ -1,8 +1,7 @@ -unit badfwrit; +unit bad_writ; fn main() -> i32 { var x: i32 = 0; @fprint(x, "bad"); return 0; } - diff --git a/fec/tests/format/okfmt.fe b/fec/tests/format/ok_forma.fe similarity index 98% rename from fec/tests/format/okfmt.fe rename to fec/tests/format/ok_forma.fe index 5cde15b..a830b93 100644 --- a/fec/tests/format/okfmt.fe +++ b/fec/tests/format/ok_forma.fe @@ -1,4 +1,4 @@ -unit okfmt; +unit ok_forma; const FMT: str = "n={} hex={x} c={c} s={s} b={b} {{ok}}\n"; @@ -28,4 +28,3 @@ fn main() -> i32 { buf5[0] == ('n' as u8) and buf5[2] == ('2' as u8) { return 0; } return 1; } - diff --git a/fec/tests/format/okprop.fe b/fec/tests/format/ok_prop.fe similarity index 84% rename from fec/tests/format/okprop.fe rename to fec/tests/format/ok_prop.fe index 09bd3b0..49a0051 100644 --- a/fec/tests/format/okprop.fe +++ b/fec/tests/format/ok_prop.fe @@ -1,6 +1,5 @@ -unit okprop; +unit ok_prop; pub fn propagate(w: io.Writer) -> !void { try @fprint(w, "a{}b", 1); } - diff --git a/fec/tests/format/oktryf.fe b/fec/tests/format/ok_try_f.fe similarity index 91% rename from fec/tests/format/oktryf.fe rename to fec/tests/format/ok_try_f.fe index 9dbb866..5429146 100644 --- a/fec/tests/format/oktryf.fe +++ b/fec/tests/format/ok_try_f.fe @@ -1,4 +1,4 @@ -unit oktryf; +unit ok_try_f; fn main() -> !void { var raw: [4]u8 = [0, 0, 0, 0]; @@ -6,4 +6,3 @@ fn main() -> !void { let w: io.Writer = io.null_writer(); try @fprint(w, "ok"); } - diff --git a/fec/tests/optional/badcatc.fe b/fec/tests/optional/badcatch.fe similarity index 92% rename from fec/tests/optional/badcatc.fe rename to fec/tests/optional/badcatch.fe index 01966ee..97ed75c 100644 --- a/fec/tests/optional/badcatc.fe +++ b/fec/tests/optional/badcatch.fe @@ -1,5 +1,5 @@ // ERROR:13:catch -unit badcatc; +unit badcatch; error E { Bad = 1, @@ -15,4 +15,3 @@ fn bad() -> i32 { }; return v; } - diff --git a/fec/tests/optional/baddefi.fe b/fec/tests/optional/baddef.fe similarity index 89% rename from fec/tests/optional/baddefi.fe rename to fec/tests/optional/baddef.fe index 2ce9671..2ff2b0f 100644 --- a/fec/tests/optional/baddefi.fe +++ b/fec/tests/optional/baddef.fe @@ -1,5 +1,5 @@ // ERROR:13:type -unit baddefi; +unit baddef; error E { Bad = 1, @@ -12,5 +12,3 @@ fn leaf() -> E!i32 { fn bad() -> i32 { return leaf() catch false; } - - diff --git a/fec/tests/optional/badidir.fe b/fec/tests/optional/baddir.fe similarity index 87% rename from fec/tests/optional/badidir.fe rename to fec/tests/optional/baddir.fe index 6d4efde..46e0715 100644 --- a/fec/tests/optional/badidir.fe +++ b/fec/tests/optional/baddir.fe @@ -1,5 +1,5 @@ // ERROR:9:optional -unit badidir; +unit baddir; struct Node { value: i32, @@ -8,4 +8,3 @@ struct Node { fn bad(p: ?^Node) -> i32 { return p.^.value; } - diff --git a/fec/tests/optional/badecod.fe b/fec/tests/optional/badercod.fe similarity index 80% rename from fec/tests/optional/badecod.fe rename to fec/tests/optional/badercod.fe index 9f4e355..ad5b34c 100644 --- a/fec/tests/optional/badecod.fe +++ b/fec/tests/optional/badercod.fe @@ -1,8 +1,7 @@ // ERROR:6:duplicate -unit badecod; +unit badercod; error E { One = 1, Two = 1, } - diff --git a/fec/tests/optional/badenam.fe b/fec/tests/optional/badernam.fe similarity index 80% rename from fec/tests/optional/badenam.fe rename to fec/tests/optional/badernam.fe index dc24789..661e8c0 100644 --- a/fec/tests/optional/badenam.fe +++ b/fec/tests/optional/badernam.fe @@ -1,8 +1,7 @@ // ERROR:6:duplicate -unit badenam; +unit badernam; error E { One = 1, One = 2, } - diff --git a/fec/tests/optional/badetyp.fe b/fec/tests/optional/badetype.fe similarity index 90% rename from fec/tests/optional/badetyp.fe rename to fec/tests/optional/badetype.fe index d847230..af5bf39 100644 --- a/fec/tests/optional/badetyp.fe +++ b/fec/tests/optional/badetype.fe @@ -1,5 +1,5 @@ // ERROR:17:error -unit badetyp; +unit badetype; error A { Bad = 1, @@ -16,5 +16,3 @@ fn leaf() -> A!i32 { fn bad() -> B!i32 { return try leaf(); } - - diff --git a/fec/tests/optional/badnullv.fe b/fec/tests/optional/badnull.fe similarity index 76% rename from fec/tests/optional/badnullv.fe rename to fec/tests/optional/badnull.fe index d2ffae9..4ed896b 100644 --- a/fec/tests/optional/badnullv.fe +++ b/fec/tests/optional/badnull.fe @@ -1,8 +1,6 @@ // ERROR:5:null -unit badnullv; +unit badnull; fn bad() -> void { let p = null; } - - diff --git a/fec/tests/optional/badorefv.fe b/fec/tests/optional/badoref.fe similarity index 76% rename from fec/tests/optional/badorefv.fe rename to fec/tests/optional/badoref.fe index 150e1c6..641206e 100644 --- a/fec/tests/optional/badorefv.fe +++ b/fec/tests/optional/badoref.fe @@ -1,8 +1,6 @@ // ERROR:5:reference -unit badorefv; +unit badoref; struct Bad { value: ?&i32, } - - diff --git a/fec/tests/optional/badorlx.fe b/fec/tests/optional/badorel.fe similarity index 89% rename from fec/tests/optional/badorlx.fe rename to fec/tests/optional/badorel.fe index 56c3b6a..3a79145 100644 --- a/fec/tests/optional/badorlx.fe +++ b/fec/tests/optional/badorel.fe @@ -1,5 +1,5 @@ // ERROR:9:non-Copy -unit badorlx; +unit badorel; struct Node { next: ?^Node, @@ -8,5 +8,3 @@ struct Node { fn bad(p: ^Node, fallback: ^Node) -> ^Node { return p.next orelse fallback; } - - diff --git a/fec/tests/optional/badprjv.fe b/fec/tests/optional/badproj.fe similarity index 88% rename from fec/tests/optional/badprjv.fe rename to fec/tests/optional/badproj.fe index 24525d2..a7d987c 100644 --- a/fec/tests/optional/badprjv.fe +++ b/fec/tests/optional/badproj.fe @@ -1,5 +1,5 @@ // ERROR:9:mem.replace -unit badprjv; +unit badproj; struct Node { next: ?^Node, @@ -9,5 +9,3 @@ fn bad(p: ^Node) -> void { let q = p.next; mem.destroy(p); } - - diff --git a/fec/tests/optional/badqmkv.fe b/fec/tests/optional/badqmark.fe similarity index 82% rename from fec/tests/optional/badqmkv.fe rename to fec/tests/optional/badqmark.fe index 183a5d2..36cdc7b 100644 --- a/fec/tests/optional/badqmkv.fe +++ b/fec/tests/optional/badqmark.fe @@ -1,9 +1,7 @@ // ERROR:6:optional -unit badqmkv; +unit badqmark; fn bad() -> i32 { let x: i32 = 1; return x.?; } - - diff --git a/fec/tests/optional/badrett.fe b/fec/tests/optional/badret.fe similarity index 88% rename from fec/tests/optional/badrett.fe rename to fec/tests/optional/badret.fe index adab03a..84842e2 100644 --- a/fec/tests/optional/badrett.fe +++ b/fec/tests/optional/badret.fe @@ -1,5 +1,5 @@ // ERROR:13:error -unit badrett; +unit badret; error A { Bad = 1, @@ -12,4 +12,3 @@ error B { fn bad() -> B!i32 { return A.Bad; } - diff --git a/fec/tests/optional/badsomev.fe b/fec/tests/optional/badsome.fe similarity index 80% rename from fec/tests/optional/badsomev.fe rename to fec/tests/optional/badsome.fe index f12958c..f35a461 100644 --- a/fec/tests/optional/badsomev.fe +++ b/fec/tests/optional/badsome.fe @@ -1,9 +1,7 @@ // ERROR:5:Some -unit badsomev; +unit badsome; fn bad() -> i32 { let x = Some(1); return x; } - - diff --git a/fec/tests/optional/badtryx.fe b/fec/tests/optional/badtry.fe similarity index 88% rename from fec/tests/optional/badtryx.fe rename to fec/tests/optional/badtry.fe index bd5ee9b..a41273e 100644 --- a/fec/tests/optional/badtryx.fe +++ b/fec/tests/optional/badtry.fe @@ -1,5 +1,5 @@ // ERROR:13:try -unit badtryx; +unit badtry; error E { Bad = 1, @@ -12,5 +12,3 @@ fn leaf() -> E!i32 { fn bad() -> i32 { return try leaf(); } - - diff --git a/fec/tests/optional/badzero1.fe b/fec/tests/optional/badzero.fe similarity index 70% rename from fec/tests/optional/badzero1.fe rename to fec/tests/optional/badzero.fe index b51d040..da86afb 100644 --- a/fec/tests/optional/badzero1.fe +++ b/fec/tests/optional/badzero.fe @@ -1,8 +1,6 @@ // ERROR:5:0 -unit badzero1; +unit badzero; error E { Zero = 0, } - - diff --git a/fec/tests/optional/okcatc.fe b/fec/tests/optional/okcatch.fe similarity index 92% rename from fec/tests/optional/okcatc.fe rename to fec/tests/optional/okcatch.fe index 13aa727..557fd12 100644 --- a/fec/tests/optional/okcatc.fe +++ b/fec/tests/optional/okcatch.fe @@ -1,4 +1,4 @@ -unit okcatc; +unit okcatch; error E { Bad = 1, @@ -14,4 +14,3 @@ fn top() -> E!i32 { }; return v; } - diff --git a/fec/tests/optional/okcatmv.fe b/fec/tests/optional/okcatmov.fe similarity index 90% rename from fec/tests/optional/okcatmv.fe rename to fec/tests/optional/okcatmov.fe index 9cdc393..c2c7b37 100644 --- a/fec/tests/optional/okcatmv.fe +++ b/fec/tests/optional/okcatmov.fe @@ -1,4 +1,4 @@ -unit okcatmv; +unit okcatmov; error E { Bad = 1, } struct Node { value: i32, } @@ -6,4 +6,3 @@ struct Node { value: i32, } fn recover(result: E!^Node, fallback: ^Node) -> ^Node { return result catch fallback; } - diff --git a/fec/tests/optional/okcvd.fe b/fec/tests/optional/okcvoid.fe similarity index 90% rename from fec/tests/optional/okcvd.fe rename to fec/tests/optional/okcvoid.fe index 1fab046..4c1c2ab 100644 --- a/fec/tests/optional/okcvd.fe +++ b/fec/tests/optional/okcvoid.fe @@ -1,4 +1,4 @@ -unit okcvd; +unit okcvoid; error E { Bad = 1, @@ -13,4 +13,3 @@ fn top() -> void { return; }; } - diff --git a/fec/tests/optional/okdefl.fe b/fec/tests/optional/okdeflt.fe similarity index 89% rename from fec/tests/optional/okdefl.fe rename to fec/tests/optional/okdeflt.fe index b5fa8a6..b62d66c 100644 --- a/fec/tests/optional/okdefl.fe +++ b/fec/tests/optional/okdeflt.fe @@ -1,4 +1,4 @@ -unit okdefl; +unit okdeflt; error E { Bad = 1, @@ -11,4 +11,3 @@ fn leaf() -> E!i32 { fn top() -> i32 { return leaf() catch 11; } - diff --git a/fec/tests/optional/okiflt.fe b/fec/tests/optional/okiflet.fe similarity index 86% rename from fec/tests/optional/okiflt.fe rename to fec/tests/optional/okiflet.fe index 7d9d14c..3192c4b 100644 --- a/fec/tests/optional/okiflt.fe +++ b/fec/tests/optional/okiflet.fe @@ -1,4 +1,4 @@ -unit okiflt; +unit okiflet; fn value(p: ?i32) -> i32 { if let Some(v) = p { @@ -6,5 +6,3 @@ fn value(p: ?i32) -> i32 { } return 0; } - - diff --git a/fec/tests/optional/okmtch.fe b/fec/tests/optional/okmatch.fe similarity index 88% rename from fec/tests/optional/okmtch.fe rename to fec/tests/optional/okmatch.fe index 6cfaa8d..6335cff 100644 --- a/fec/tests/optional/okmtch.fe +++ b/fec/tests/optional/okmatch.fe @@ -1,4 +1,4 @@ -unit okmtch; +unit okmatch; fn value(p: ?i32) -> i32 { match p { @@ -6,5 +6,3 @@ fn value(p: ?i32) -> i32 { None => { return 0; } } } - - diff --git a/fec/tests/optional/oknull1.fe b/fec/tests/optional/oknull.fe similarity index 90% rename from fec/tests/optional/oknull1.fe rename to fec/tests/optional/oknull.fe index ce22d95..c12a917 100644 --- a/fec/tests/optional/oknull1.fe +++ b/fec/tests/optional/oknull.fe @@ -1,4 +1,4 @@ -unit oknull1; +unit oknull; struct Node { value: i32, } @@ -8,5 +8,3 @@ fn test() -> bool { let p: ?^Node = null; return accepts(p); } - - diff --git a/fec/tests/optional/okorel.fe b/fec/tests/optional/okorelse.fe similarity index 77% rename from fec/tests/optional/okorel.fe rename to fec/tests/optional/okorelse.fe index f49a0f5..9ff537e 100644 --- a/fec/tests/optional/okorel.fe +++ b/fec/tests/optional/okorelse.fe @@ -1,6 +1,5 @@ -unit okorel; +unit okorelse; fn value(p: ?i32) -> i32 { return p orelse 9; } - diff --git a/fec/tests/optional/okptvw.fe b/fec/tests/optional/okpatvw.fe similarity index 93% rename from fec/tests/optional/okptvw.fe rename to fec/tests/optional/okpatvw.fe index a046b5e..5797b46 100644 --- a/fec/tests/optional/okptvw.fe +++ b/fec/tests/optional/okpatvw.fe @@ -1,4 +1,4 @@ -unit okptvw; +unit okpatvw; struct Node { value: i32, } @@ -10,5 +10,3 @@ fn keep(p: ?^Node) -> void { let owned = mem.replace(&mut slot, null).?; mem.destroy(owned); } - - diff --git a/fec/tests/optional/okproj1.fe b/fec/tests/optional/okproj.fe similarity index 84% rename from fec/tests/optional/okproj1.fe rename to fec/tests/optional/okproj.fe index 4bcca85..f54768a 100644 --- a/fec/tests/optional/okproj1.fe +++ b/fec/tests/optional/okproj.fe @@ -1,4 +1,4 @@ -unit okproj1; +unit okproj; struct Node { value: i32, @@ -7,5 +7,3 @@ struct Node { fn touch(p: ?&mut Node) -> void { p.?.value = 7; } - - diff --git a/fec/tests/optional/okrepl2.fe b/fec/tests/optional/okrepl.fe similarity index 90% rename from fec/tests/optional/okrepl2.fe rename to fec/tests/optional/okrepl.fe index 1bab0bd..2710132 100644 --- a/fec/tests/optional/okrepl2.fe +++ b/fec/tests/optional/okrepl.fe @@ -1,4 +1,4 @@ -unit okrepl2; +unit okrepl; struct Node { value: i32, @@ -9,5 +9,3 @@ fn take(p: ?^Node) -> void { let n = mem.replace(&mut q, null).?; mem.destroy(n); } - - diff --git a/fec/tests/optional/oktrd2.fe b/fec/tests/optional/oktrdef.fe similarity index 90% rename from fec/tests/optional/oktrd2.fe rename to fec/tests/optional/oktrdef.fe index f0361df..1b0831c 100644 --- a/fec/tests/optional/oktrd2.fe +++ b/fec/tests/optional/oktrdef.fe @@ -1,4 +1,4 @@ -unit oktrd2; +unit oktrdef; error E { Bad = 1, @@ -12,5 +12,3 @@ fn top() -> E!i32 { defer { let x: i32 = 1; } return try leaf(); } - - diff --git a/fec/tests/optional/oktry2.fe b/fec/tests/optional/oktry.fe similarity index 90% rename from fec/tests/optional/oktry2.fe rename to fec/tests/optional/oktry.fe index 6140223..f0066db 100644 --- a/fec/tests/optional/oktry2.fe +++ b/fec/tests/optional/oktry.fe @@ -1,4 +1,4 @@ -unit oktry2; +unit oktry; error E { Bad = 1, @@ -12,5 +12,3 @@ fn leaf(ok: bool) -> E!i32 { fn top() -> E!i32 { return try leaf(true); } - - diff --git a/fec/tests/own/badclos.fe b/fec/tests/own/bad_clos.fe similarity index 94% rename from fec/tests/own/badclos.fe rename to fec/tests/own/bad_clos.fe index 447c0f6..28ff3e4 100644 --- a/fec/tests/own/badclos.fe +++ b/fec/tests/own/bad_clos.fe @@ -1,4 +1,4 @@ -unit badclos; +unit bad_clos; struct FileLike { handle: i32, @@ -11,4 +11,3 @@ fn bad() -> !void { try file.close(); file.close(); } - diff --git a/fec/tests/own/badcond.fe b/fec/tests/own/bad_cond.fe similarity index 89% rename from fec/tests/own/badcond.fe rename to fec/tests/own/bad_cond.fe index 2f5916d..f46cfe4 100644 --- a/fec/tests/own/badcond.fe +++ b/fec/tests/own/bad_cond.fe @@ -1,4 +1,4 @@ -unit badcond; +unit bad_cond; fn take(p: ^i32) -> void { mem.destroy(p); } @@ -6,4 +6,3 @@ fn bad(p: ^i32, flag: bool) -> void { if flag { take(p); } p.^ = 3; } - diff --git a/fec/tests/own/baddbl.fe b/fec/tests/own/bad_dbl.fe similarity index 83% rename from fec/tests/own/baddbl.fe rename to fec/tests/own/bad_dbl.fe index 84f3912..a990673 100644 --- a/fec/tests/own/baddbl.fe +++ b/fec/tests/own/bad_dbl.fe @@ -1,7 +1,6 @@ -unit baddbl; +unit bad_dbl; fn bad(p: ^i32) -> void { mem.destroy(p); mem.destroy(p); } - diff --git a/fec/tests/own/baddest.fe b/fec/tests/own/bad_dest.fe similarity index 76% rename from fec/tests/own/baddest.fe rename to fec/tests/own/bad_dest.fe index 11d462b..8cedd19 100644 --- a/fec/tests/own/baddest.fe +++ b/fec/tests/own/bad_dest.fe @@ -1,6 +1,5 @@ -unit baddest; +unit bad_dest; fn bad(x: i32) -> void { mem.destroy(x); } - diff --git a/fec/tests/own/baddrop.fe b/fec/tests/own/bad_drop.fe similarity index 90% rename from fec/tests/own/baddrop.fe rename to fec/tests/own/bad_drop.fe index 5a4e74d..ad9e9cc 100644 --- a/fec/tests/own/baddrop.fe +++ b/fec/tests/own/bad_drop.fe @@ -1,4 +1,4 @@ -unit baddrop; +unit bad_drop; struct Box { value: i32, @@ -9,4 +9,3 @@ fn bad() -> void { var b: Box = Box{ value: 1 }; b.drop(); } - diff --git a/fec/tests/own/badlop1.fe b/fec/tests/own/bad_loop.fe similarity index 88% rename from fec/tests/own/badlop1.fe rename to fec/tests/own/bad_loop.fe index 7dc06ff..c7fd822 100644 --- a/fec/tests/own/badlop1.fe +++ b/fec/tests/own/bad_loop.fe @@ -1,8 +1,7 @@ -unit badlop1; +unit bad_loop; fn take(p: ^i32) -> void { mem.destroy(p); } fn bad(p: ^i32, again: bool) -> void { while again { take(p); } } - diff --git a/fec/tests/own/badmv1.fe b/fec/tests/own/bad_move.fe similarity index 87% rename from fec/tests/own/badmv1.fe rename to fec/tests/own/bad_move.fe index 8f53bcc..15d25fb 100644 --- a/fec/tests/own/badmv1.fe +++ b/fec/tests/own/bad_move.fe @@ -1,4 +1,4 @@ -unit badmv1; +unit bad_move; fn take(p: ^i32) -> void { mem.destroy(p); } @@ -6,4 +6,3 @@ fn twice(p: ^i32) -> void { take(p); take(p); } - diff --git a/fec/tests/own/badproj.fe b/fec/tests/own/bad_proj.fe similarity index 88% rename from fec/tests/own/badproj.fe rename to fec/tests/own/bad_proj.fe index e756adf..0c50987 100644 --- a/fec/tests/own/badproj.fe +++ b/fec/tests/own/bad_proj.fe @@ -1,4 +1,4 @@ -unit badproj; +unit bad_proj; struct Holder { p: ^i32 } fn take(p: ^i32) -> void { mem.destroy(p); } @@ -6,4 +6,3 @@ fn take(p: ^i32) -> void { mem.destroy(p); } fn bad(h: Holder) -> void { take(h.p); } - diff --git a/fec/tests/own/badargu.fe b/fec/tests/own/badarg.fe similarity index 90% rename from fec/tests/own/badargu.fe rename to fec/tests/own/badarg.fe index 55d092d..7e50114 100644 --- a/fec/tests/own/badargu.fe +++ b/fec/tests/own/badarg.fe @@ -1,5 +1,5 @@ // ERROR:8:derived from a parameter -unit badargu; +unit badarg; struct Box { value: i32, @@ -8,5 +8,3 @@ struct Box { return other; } } - - diff --git a/fec/tests/own/badinit.fe b/fec/tests/own/badbinit.fe similarity index 90% rename from fec/tests/own/badinit.fe rename to fec/tests/own/badbinit.fe index 049641b..b6eb632 100644 --- a/fec/tests/own/badinit.fe +++ b/fec/tests/own/badbinit.fe @@ -1,5 +1,5 @@ // ERROR:9:initialized -unit badinit; +unit badbinit; fn bad(assign: bool) -> i32 { var value: i32; @@ -8,4 +8,3 @@ fn bad(assign: bool) -> i32 { } return value; } - diff --git a/fec/tests/own/badbrmv.fe b/fec/tests/own/badbrmov.fe similarity index 89% rename from fec/tests/own/badbrmv.fe rename to fec/tests/own/badbrmov.fe index d0fc104..f07825d 100644 --- a/fec/tests/own/badbrmv.fe +++ b/fec/tests/own/badbrmov.fe @@ -1,5 +1,5 @@ // ERROR:8:move -unit badbrmv; +unit badbrmov; fn bad(p: ^i32, consume: bool) -> void { if consume { @@ -7,4 +7,3 @@ fn bad(p: ^i32, consume: bool) -> void { } mem.destroy(p); } - diff --git a/fec/tests/own/baddefr.fe b/fec/tests/own/baddefer.fe similarity index 92% rename from fec/tests/own/baddefr.fe rename to fec/tests/own/baddefer.fe index 47a991f..faf7839 100644 --- a/fec/tests/own/baddefr.fe +++ b/fec/tests/own/baddefer.fe @@ -1,5 +1,5 @@ // ERROR:10:borrow -unit baddefr; +unit baddefer; fn read(r: &i32) -> i32 { return r.^; } @@ -10,4 +10,3 @@ fn bad() -> i32 { x = 1; return x; } - diff --git a/fec/tests/own/badfmem.fe b/fec/tests/own/badfld.fe similarity index 78% rename from fec/tests/own/badfmem.fe rename to fec/tests/own/badfld.fe index 71eec2c..d4090ff 100644 --- a/fec/tests/own/badfmem.fe +++ b/fec/tests/own/badfld.fe @@ -1,7 +1,6 @@ // ERROR:5:reference -unit badfmem; +unit badfld; struct Bad { value: &i32, } - diff --git a/fec/tests/own/badglobx.fe b/fec/tests/own/badglob.fe similarity index 84% rename from fec/tests/own/badglobx.fe rename to fec/tests/own/badglob.fe index 3afdcc9..41a107c 100644 --- a/fec/tests/own/badglobx.fe +++ b/fec/tests/own/badglob.fe @@ -1,5 +1,5 @@ // ERROR:7:global -unit badglobx; +unit badglob; var VALUE: i32 = 0; @@ -7,5 +7,3 @@ fn bad() -> i32 { let r = &VALUE; return r.^; } - - diff --git a/fec/tests/own/badinvr.fe b/fec/tests/own/badinv.fe similarity index 88% rename from fec/tests/own/badinvr.fe rename to fec/tests/own/badinv.fe index 3338eed..88f483c 100644 --- a/fec/tests/own/badinvr.fe +++ b/fec/tests/own/badinv.fe @@ -1,5 +1,5 @@ // ERROR:6:borrow -unit badinvr; +unit badinv; fn bad(p: ^i32, q: ^i32) -> void { let r = &p; @@ -7,5 +7,3 @@ fn bad(p: ^i32, q: ^i32) -> void { let keep = r; mem.destroy(p); } - - diff --git a/fec/tests/own/badlp2.fe b/fec/tests/own/badloop.fe similarity index 90% rename from fec/tests/own/badlp2.fe rename to fec/tests/own/badloop.fe index 0d403b6..1cb6a4f 100644 --- a/fec/tests/own/badlp2.fe +++ b/fec/tests/own/badloop.fe @@ -1,5 +1,5 @@ // ERROR:6:move -unit badlp2; +unit badloop; fn bad(p: ^i32, again: bool) -> void { while again { @@ -7,4 +7,3 @@ fn bad(p: ^i32, again: bool) -> void { } mem.destroy(p); } - diff --git a/fec/tests/own/badmv2.fe b/fec/tests/own/badmove.fe similarity index 90% rename from fec/tests/own/badmv2.fe rename to fec/tests/own/badmove.fe index da71598..e746062 100644 --- a/fec/tests/own/badmv2.fe +++ b/fec/tests/own/badmove.fe @@ -1,5 +1,5 @@ // ERROR:8:borrow -unit badmv2; +unit badmove; fn take(p: ^i32) -> void { mem.destroy(p); } @@ -8,4 +8,3 @@ fn bad(p: ^i32) -> void { take(p); let q = r; } - diff --git a/fec/tests/own/badmutv.fe b/fec/tests/own/badmut.fe similarity index 87% rename from fec/tests/own/badmutv.fe rename to fec/tests/own/badmut.fe index 95741c0..90aab1f 100644 --- a/fec/tests/own/badmutv.fe +++ b/fec/tests/own/badmut.fe @@ -1,5 +1,5 @@ // ERROR:7:borrow -unit badmutv; +unit badmut; fn bad() -> i32 { var x: i32 = 0; @@ -8,5 +8,3 @@ fn bad() -> i32 { r.^ = 2; return x; } - - diff --git a/fec/tests/own/badmutx.fe b/fec/tests/own/badmut2.fe similarity index 89% rename from fec/tests/own/badmutx.fe rename to fec/tests/own/badmut2.fe index 34fa6e9..790e251 100644 --- a/fec/tests/own/badmutx.fe +++ b/fec/tests/own/badmut2.fe @@ -1,5 +1,5 @@ // ERROR:7:borrow -unit badmutx; +unit badmut2; fn bad() -> i32 { var x: i32 = 0; @@ -9,5 +9,3 @@ fn bad() -> i32 { b.^ = 2; return x; } - - diff --git a/fec/tests/own/badptrx.fe b/fec/tests/own/badptr.fe similarity index 79% rename from fec/tests/own/badptrx.fe rename to fec/tests/own/badptr.fe index 1426db3..36e9490 100644 --- a/fec/tests/own/badptrx.fe +++ b/fec/tests/own/badptr.fe @@ -1,8 +1,6 @@ // ERROR:4:reference -unit badptrx; +unit badptr; fn bad(p: *&i32) -> void { return; } - - diff --git a/fec/tests/own/badretv.fe b/fec/tests/own/badret.fe similarity index 83% rename from fec/tests/own/badretv.fe rename to fec/tests/own/badret.fe index 774a760..5a9a777 100644 --- a/fec/tests/own/badretv.fe +++ b/fec/tests/own/badret.fe @@ -1,8 +1,7 @@ // ERROR:6:reference -unit badretv; +unit badret; fn bad() -> &i32 { let x: i32 = 1; return &x; } - diff --git a/fec/tests/own/badrfie.fe b/fec/tests/own/badrfld.fe similarity index 90% rename from fec/tests/own/badrfie.fe rename to fec/tests/own/badrfld.fe index fd366aa..65cb8b0 100644 --- a/fec/tests/own/badrfie.fe +++ b/fec/tests/own/badrfld.fe @@ -1,5 +1,5 @@ // ERROR:9:borrow -unit badrfie; +unit badrfld; struct Pair { a: i32, b: i32, } @@ -9,5 +9,3 @@ fn bad() -> void { p.b = 3; left.^ = 4; } - - diff --git a/fec/tests/own/badri2.fe b/fec/tests/own/badridx.fe similarity index 90% rename from fec/tests/own/badri2.fe rename to fec/tests/own/badridx.fe index a6987b3..d7597c5 100644 --- a/fec/tests/own/badri2.fe +++ b/fec/tests/own/badridx.fe @@ -1,5 +1,5 @@ // ERROR:7:borrow -unit badri2; +unit badridx; fn bad() -> void { var xs: [2]i32 = [1, 2]; @@ -8,5 +8,3 @@ fn bad() -> void { a.^ = 3; b.^ = 4; } - - diff --git a/fec/tests/own/badscp1.fe b/fec/tests/own/badscop.fe similarity index 92% rename from fec/tests/own/badscp1.fe rename to fec/tests/own/badscop.fe index a7893fe..3bbeded 100644 --- a/fec/tests/own/badscp1.fe +++ b/fec/tests/own/badscop.fe @@ -1,5 +1,5 @@ // ERROR:9:reference -unit badscp1; +unit badscop; fn bad(cond: bool) -> i32 { let outer: i32 = 0; @@ -10,5 +10,3 @@ fn bad(cond: bool) -> i32 { } return r.^; } - - diff --git a/fec/tests/own/badselfw.fe b/fec/tests/own/badself.fe similarity index 87% rename from fec/tests/own/badselfw.fe rename to fec/tests/own/badself.fe index 2e2795d..fe80c84 100644 --- a/fec/tests/own/badselfw.fe +++ b/fec/tests/own/badself.fe @@ -1,5 +1,5 @@ // ERROR:8:self -unit badselfw; +unit badself; struct Box { value: i32, @@ -8,5 +8,3 @@ struct Box { return &self.value; } } - - diff --git a/fec/tests/own/badshw1.fe b/fec/tests/own/badshwr.fe similarity index 90% rename from fec/tests/own/badshw1.fe rename to fec/tests/own/badshwr.fe index c1c2d11..015d14f 100644 --- a/fec/tests/own/badshw1.fe +++ b/fec/tests/own/badshwr.fe @@ -1,5 +1,5 @@ // ERROR:9:borrow -unit badshw1; +unit badshwr; fn read(r: &i32) -> i32 { return r.^; } @@ -9,5 +9,3 @@ fn bad() -> i32 { x = 1; return read(r); } - - diff --git a/fec/tests/own/badtwo2.fe b/fec/tests/own/badtwo.fe similarity index 82% rename from fec/tests/own/badtwo2.fe rename to fec/tests/own/badtwo.fe index 431f7e9..dcdfa0e 100644 --- a/fec/tests/own/badtwo2.fe +++ b/fec/tests/own/badtwo.fe @@ -1,8 +1,6 @@ // ERROR:5:reference -unit badtwo2; +unit badtwo; fn choose(a: &i32, b: &i32) -> &i32 { return a; } - - diff --git a/fec/tests/own/badup1.fe b/fec/tests/own/badup.fe similarity index 89% rename from fec/tests/own/badup1.fe rename to fec/tests/own/badup.fe index a7b363c..34f2b7e 100644 --- a/fec/tests/own/badup1.fe +++ b/fec/tests/own/badup.fe @@ -1,5 +1,5 @@ // ERROR:8:mut -unit badup1; +unit badup; struct Box { value: i32, @@ -8,5 +8,3 @@ struct Box { return &mut self.value; } } - - diff --git a/fec/tests/own/badweak2.fe b/fec/tests/own/badweak.fe similarity index 87% rename from fec/tests/own/badweak2.fe rename to fec/tests/own/badweak.fe index 645e3fa..8da6bc8 100644 --- a/fec/tests/own/badweak2.fe +++ b/fec/tests/own/badweak.fe @@ -1,5 +1,5 @@ // ERROR:7:mut -unit badweak2; +unit badweak; fn bad() -> void { var x: i32 = 0; @@ -7,5 +7,3 @@ fn bad() -> void { let s: &i32 = m; let v = s.^; } - - diff --git a/fec/tests/own/okdefer1.fe b/fec/tests/own/ok_defer.fe similarity index 80% rename from fec/tests/own/okdefer1.fe rename to fec/tests/own/ok_defer.fe index 518a806..e4e2fef 100644 --- a/fec/tests/own/okdefer1.fe +++ b/fec/tests/own/ok_defer.fe @@ -1,6 +1,5 @@ -unit okdefer1; +unit ok_defer; pub fn cleanup(p: ^i32) -> void { defer { mem.destroy(p); } } - diff --git a/fec/tests/own/okowned.fe b/fec/tests/own/ok_owned.fe similarity index 91% rename from fec/tests/own/okowned.fe rename to fec/tests/own/ok_owned.fe index 96581e5..2881628 100644 --- a/fec/tests/own/okowned.fe +++ b/fec/tests/own/ok_owned.fe @@ -1,4 +1,4 @@ -unit okowned; +unit ok_owned; fn main() -> !void { var p: ^i32 = try mem.create(0); @@ -7,4 +7,3 @@ fn main() -> !void { let value: i32 = p.^; defer { mem.destroy(p); } } - diff --git a/fec/tests/own/okbrch.fe b/fec/tests/own/okbranch.fe similarity index 93% rename from fec/tests/own/okbrch.fe rename to fec/tests/own/okbranch.fe index e68873e..f6964b8 100644 --- a/fec/tests/own/okbrch.fe +++ b/fec/tests/own/okbranch.fe @@ -1,4 +1,4 @@ -unit okbrch; +unit okbranch; fn read(r: &i32) -> i32 { return r.^; } @@ -13,5 +13,3 @@ fn test(cond: bool) -> i32 { x += 1; return x; } - - diff --git a/fec/tests/own/okdefer2.fe b/fec/tests/own/okdefer.fe similarity index 92% rename from fec/tests/own/okdefer2.fe rename to fec/tests/own/okdefer.fe index 5185a16..1556b46 100644 --- a/fec/tests/own/okdefer2.fe +++ b/fec/tests/own/okdefer.fe @@ -1,4 +1,4 @@ -unit okdefer2; +unit okdefer; fn read(r: &i32) -> i32 { return r.^; } @@ -11,4 +11,3 @@ fn test() -> i32 { x += 1; return x; } - diff --git a/fec/tests/own/okglobc.fe b/fec/tests/own/okglobcp.fe similarity index 89% rename from fec/tests/own/okglobc.fe rename to fec/tests/own/okglobcp.fe index 6760865..e0c3e95 100644 --- a/fec/tests/own/okglobc.fe +++ b/fec/tests/own/okglobcp.fe @@ -1,4 +1,4 @@ -unit okglobc; +unit okglobcp; var VALUE: i32 = 7; @@ -8,4 +8,3 @@ fn test() -> i32 { let local = VALUE; return read(&local); } - diff --git a/fec/tests/own/oklast1.fe b/fec/tests/own/oklast.fe similarity index 86% rename from fec/tests/own/oklast1.fe rename to fec/tests/own/oklast.fe index dfb7897..2e1e9f1 100644 --- a/fec/tests/own/oklast1.fe +++ b/fec/tests/own/oklast.fe @@ -1,4 +1,4 @@ -unit oklast1; +unit oklast; fn test() -> i32 { var x: i32 = 0; @@ -7,5 +7,3 @@ fn test() -> i32 { x += 1; return x; } - - diff --git a/fec/tests/own/okr8fr.fe b/fec/tests/own/okr8free.fe similarity index 92% rename from fec/tests/own/okr8fr.fe rename to fec/tests/own/okr8free.fe index 49c2ac8..0ae189e 100644 --- a/fec/tests/own/okr8fr.fe +++ b/fec/tests/own/okr8free.fe @@ -1,4 +1,4 @@ -unit okr8fr; +unit okr8free; fn head(s: []u8) -> &u8 { return &s[0]; @@ -11,5 +11,3 @@ fn test() -> u8 { a[0] = 7 as u8; return v; } - - diff --git a/fec/tests/own/okr8jn.fe b/fec/tests/own/okr8join.fe similarity index 94% rename from fec/tests/own/okr8jn.fe rename to fec/tests/own/okr8join.fe index 9395a1e..b2e67dd 100644 --- a/fec/tests/own/okr8jn.fe +++ b/fec/tests/own/okr8join.fe @@ -1,4 +1,4 @@ -unit okr8jn; +unit okr8join; fn select(s: str, from_param: bool) -> str { if from_param { return s; } @@ -12,5 +12,3 @@ fn test() -> u8 { bytes[0] = 8 as u8; return value; } - - diff --git a/fec/tests/own/okr8mt.fe b/fec/tests/own/okr8meth.fe similarity index 93% rename from fec/tests/own/okr8mt.fe rename to fec/tests/own/okr8meth.fe index 271c723..8f49258 100644 --- a/fec/tests/own/okr8mt.fe +++ b/fec/tests/own/okr8meth.fe @@ -1,4 +1,4 @@ -unit okr8mt; +unit okr8meth; struct Box { value: i32, @@ -15,5 +15,3 @@ fn test() -> i32 { b.value = 5; return v; } - - diff --git a/fec/tests/own/okr8st.fe b/fec/tests/own/okr8stat.fe similarity index 73% rename from fec/tests/own/okr8st.fe rename to fec/tests/own/okr8stat.fe index 6158944..5db67d4 100644 --- a/fec/tests/own/okr8st.fe +++ b/fec/tests/own/okr8stat.fe @@ -1,7 +1,5 @@ -unit okr8st; +unit okr8stat; fn name() -> str { return "main"; } - - diff --git a/fec/tests/own/okrbor1.fe b/fec/tests/own/okrebor.fe similarity index 90% rename from fec/tests/own/okrbor1.fe rename to fec/tests/own/okrebor.fe index 0569f27..b92ebf8 100644 --- a/fec/tests/own/okrbor1.fe +++ b/fec/tests/own/okrebor.fe @@ -1,4 +1,4 @@ -unit okrbor1; +unit okrebor; fn read(r: &i32) -> i32 { return r.^; } @@ -9,5 +9,3 @@ fn test() -> i32 { r.^ = v + 1; return r.^; } - - diff --git a/fec/tests/own/okrtls.fe b/fec/tests/own/okrtlast.fe similarity index 91% rename from fec/tests/own/okrtls.fe rename to fec/tests/own/okrtlast.fe index e917db9..bd87c4e 100644 --- a/fec/tests/own/okrtls.fe +++ b/fec/tests/own/okrtlast.fe @@ -1,4 +1,4 @@ -unit okrtls; +unit okrtlast; struct Pair { a: i32, b: i32, } @@ -9,5 +9,3 @@ fn test() -> i32 { p.b = 4; return p.a + p.b; } - - diff --git a/fec/tests/own/okshar1.fe b/fec/tests/own/okshare.fe similarity index 90% rename from fec/tests/own/okshar1.fe rename to fec/tests/own/okshare.fe index a82bdd0..1740394 100644 --- a/fec/tests/own/okshar1.fe +++ b/fec/tests/own/okshare.fe @@ -1,4 +1,4 @@ -unit okshar1; +unit okshare; fn add(a: &i32, b: &i32) -> i32 { return a.^ + b.^; @@ -10,5 +10,3 @@ fn test() -> i32 { let b = &x; return add(a, b); } - - diff --git a/fec/tests/own/okslre1.fe b/fec/tests/own/okslreb.fe similarity index 91% rename from fec/tests/own/okslre1.fe rename to fec/tests/own/okslreb.fe index 1f3aef6..7b8a6a7 100644 --- a/fec/tests/own/okslre1.fe +++ b/fec/tests/own/okslreb.fe @@ -1,4 +1,4 @@ -unit okslre1; +unit okslreb; fn first(s: []u8) -> u8 { return s[0]; } @@ -9,5 +9,3 @@ fn test() -> u8 { s[0] = 9 as u8; return v; } - - diff --git a/fec/tests/own/okstat1.fe b/fec/tests/own/okstatic.fe similarity index 86% rename from fec/tests/own/okstat1.fe rename to fec/tests/own/okstatic.fe index e231e3c..ae2487e 100644 --- a/fec/tests/own/okstat1.fe +++ b/fec/tests/own/okstatic.fe @@ -1,4 +1,4 @@ -unit okstat1; +unit okstatic; static VALUE: i32 = 7; @@ -9,5 +9,3 @@ fn get() -> &i32 { fn test() -> i32 { return get().^; } - - diff --git a/fec/tests/own/oktemp1.fe b/fec/tests/own/oktemp.fe similarity index 89% rename from fec/tests/own/oktemp1.fe rename to fec/tests/own/oktemp.fe index c382b89..d88b1f7 100644 --- a/fec/tests/own/oktemp1.fe +++ b/fec/tests/own/oktemp.fe @@ -1,4 +1,4 @@ -unit oktemp1; +unit oktemp; fn read(r: &i32) -> i32 { return r.^; } @@ -8,5 +8,3 @@ fn test() -> i32 { x += 1; return v + x; } - - diff --git a/fec/tests/own/oktrim1.fe b/fec/tests/own/oktrim.fe similarity index 78% rename from fec/tests/own/oktrim1.fe rename to fec/tests/own/oktrim.fe index f1e403c..cf416ac 100644 --- a/fec/tests/own/oktrim1.fe +++ b/fec/tests/own/oktrim.fe @@ -1,7 +1,5 @@ -unit oktrim1; +unit oktrim; fn trimmed(line: str) -> str { return line.trim(); } - - diff --git a/fec/tests/own/okwcal1.fe b/fec/tests/own/okwcall.fe similarity index 91% rename from fec/tests/own/okwcal1.fe rename to fec/tests/own/okwcall.fe index a81f447..a53efe2 100644 --- a/fec/tests/own/okwcal1.fe +++ b/fec/tests/own/okwcall.fe @@ -1,4 +1,4 @@ -unit okwcal1; +unit okwcall; fn read(r: &i32) -> i32 { return r.^; } @@ -9,5 +9,3 @@ fn test() -> i32 { m.^ = value + 1; return m.^; } - - diff --git a/fec/tests/types/badarith.fe b/fec/tests/types/bad_ari.fe similarity index 85% rename from fec/tests/types/badarith.fe rename to fec/tests/types/bad_ari.fe index 424e3cf..bdf4619 100644 --- a/fec/tests/types/badarith.fe +++ b/fec/tests/types/bad_ari.fe @@ -1,4 +1,4 @@ -unit badarith; +unit bad_ari; fn add(a: i32, b: i32) -> i32 { return a + b; @@ -7,4 +7,3 @@ fn add(a: i32, b: i32) -> i32 { fn main() -> i32 { return add(1); } - diff --git a/fec/tests/types/badasgn.fe b/fec/tests/types/bad_asgn.fe similarity index 84% rename from fec/tests/types/badasgn.fe rename to fec/tests/types/bad_asgn.fe index eae96b2..3e921bc 100644 --- a/fec/tests/types/badasgn.fe +++ b/fec/tests/types/bad_asgn.fe @@ -1,8 +1,7 @@ -unit badasgn; +unit bad_asgn; fn main() -> i32 { let value: i32 = 1; value = 2; return value; } - diff --git a/fec/tests/types/badcast.fe b/fec/tests/types/bad_cast.fe similarity index 81% rename from fec/tests/types/badcast.fe rename to fec/tests/types/bad_cast.fe index 42ec79f..0eafcfc 100644 --- a/fec/tests/types/badcast.fe +++ b/fec/tests/types/bad_cast.fe @@ -1,7 +1,6 @@ -unit badcast; +unit bad_cast; fn main() -> i32 { let x: i32 = true as i32; return x; } - diff --git a/fec/tests/types/badcond.fe b/fec/tests/types/bad_cond.fe similarity index 79% rename from fec/tests/types/badcond.fe rename to fec/tests/types/bad_cond.fe index 5773959..a3968f1 100644 --- a/fec/tests/types/badcond.fe +++ b/fec/tests/types/bad_cond.fe @@ -1,7 +1,6 @@ -unit badcond; +unit bad_cond; fn main() -> i32 { if 1 { return 0; } return 1; } - diff --git a/fec/tests/types/badmlet.fe b/fec/tests/types/bad_mlet.fe similarity index 84% rename from fec/tests/types/badmlet.fe rename to fec/tests/types/bad_mlet.fe index 7283053..a3960b0 100644 --- a/fec/tests/types/badmlet.fe +++ b/fec/tests/types/bad_mlet.fe @@ -1,7 +1,6 @@ -unit badmlet; +unit bad_mlet; fn bad() -> void { var raw: [2]u8 = [1, 2]; let s: []mut u8 = raw[..]; } - diff --git a/fec/tests/types/badretu.fe b/fec/tests/types/bad_ret.fe similarity index 72% rename from fec/tests/types/badretu.fe rename to fec/tests/types/bad_ret.fe index bdeb958..a151ab3 100644 --- a/fec/tests/types/badretu.fe +++ b/fec/tests/types/bad_ret.fe @@ -1,6 +1,5 @@ -unit badretu; +unit bad_ret; fn main() -> i32 { return true; } - diff --git a/fec/tests/types/badshwr.fe b/fec/tests/types/bad_shwr.fe similarity index 74% rename from fec/tests/types/badshwr.fe rename to fec/tests/types/bad_shwr.fe index d46977b..7c7881f 100644 --- a/fec/tests/types/badshwr.fe +++ b/fec/tests/types/bad_shwr.fe @@ -1,6 +1,5 @@ -unit badshwr; +unit bad_shwr; fn bad(s: []u8) -> void { s[0] = 1; } - diff --git a/fec/tests/types/badtype.fe b/fec/tests/types/bad_type.fe similarity index 86% rename from fec/tests/types/badtype.fe rename to fec/tests/types/bad_type.fe index c3fc863..15a4aca 100644 --- a/fec/tests/types/badtype.fe +++ b/fec/tests/types/bad_type.fe @@ -1,4 +1,4 @@ -unit badtype; +unit bad_type; fn add(a: i32, b: i32) -> i32 { return a + b; @@ -7,4 +7,3 @@ fn add(a: i32, b: i32) -> i32 { fn main() -> i32 { return add(true, 1); } - diff --git a/fec/tests/types/badunit.fe b/fec/tests/types/bad_unit.fe similarity index 78% rename from fec/tests/types/badunit.fe rename to fec/tests/types/bad_unit.fe index 7074a19..99d8bbd 100644 --- a/fec/tests/types/badunit.fe +++ b/fec/tests/types/bad_unit.fe @@ -1,8 +1,6 @@ -unit badunit; +unit bad_unit; fn main() -> i32 { var value: i32; return value; } - - diff --git a/fec/tests/types/badunk.fe b/fec/tests/types/bad_unk.fe similarity index 77% rename from fec/tests/types/badunk.fe rename to fec/tests/types/bad_unk.fe index ec28188..b868c3b 100644 --- a/fec/tests/types/badunk.fe +++ b/fec/tests/types/bad_unk.fe @@ -1,6 +1,5 @@ -unit badunk; +unit bad_unk; fn main() -> i32 { return missing_name; } - diff --git a/fec/tests/types/badvoid.fe b/fec/tests/types/bad_void.fe similarity index 86% rename from fec/tests/types/badvoid.fe rename to fec/tests/types/bad_void.fe index 9ab0cae..bf3ce1f 100644 --- a/fec/tests/types/badvoid.fe +++ b/fec/tests/types/bad_void.fe @@ -1,4 +1,4 @@ -unit badvoid; +unit bad_void; fn noop() { return; @@ -8,4 +8,3 @@ fn main() -> i32 { let value: i32 = noop(); return value; } - diff --git a/fec/tests/types/badarry.fe b/fec/tests/types/badarr.fe similarity index 82% rename from fec/tests/types/badarry.fe rename to fec/tests/types/badarr.fe index ad4072f..11007cb 100644 --- a/fec/tests/types/badarry.fe +++ b/fec/tests/types/badarr.fe @@ -1,6 +1,5 @@ -unit badarry; +unit badarr; fn main() -> i32 { let a: [2]i32 = [1, true, 3]; return a[0]; } - diff --git a/fec/tests/types/badbyte.fe b/fec/tests/types/badchar.fe similarity index 79% rename from fec/tests/types/badbyte.fe rename to fec/tests/types/badchar.fe index de2a923..9d1a1d4 100644 --- a/fec/tests/types/badbyte.fe +++ b/fec/tests/types/badchar.fe @@ -1,7 +1,6 @@ -unit badbyte; +unit badchar; fn main() -> i32 { let u: u8 = 'A'; return u; } - diff --git a/fec/tests/types/badcyc.fe b/fec/tests/types/badcycle.fe similarity index 82% rename from fec/tests/types/badcyc.fe rename to fec/tests/types/badcycle.fe index 1b79348..532ea1e 100644 --- a/fec/tests/types/badcyc.fe +++ b/fec/tests/types/badcycle.fe @@ -1,8 +1,6 @@ -unit badcyc; +unit badcycle; struct A { b: B, } struct B { a: A, } fn main() -> i32 { return 0; } - - diff --git a/fec/tests/types/badfiel.fe b/fec/tests/types/badfield.fe similarity index 89% rename from fec/tests/types/badfiel.fe rename to fec/tests/types/badfield.fe index 6c0d2fd..4fa664c 100644 --- a/fec/tests/types/badfiel.fe +++ b/fec/tests/types/badfield.fe @@ -1,4 +1,4 @@ -unit badfiel; +unit badfield; struct Point { x: i32, y: i32, } fn main() -> i32 { @@ -6,4 +6,3 @@ fn main() -> i32 { p.x = 3; return p.x; } - diff --git a/fec/tests/types/badfmem.fe b/fec/tests/types/badfld.fe similarity index 87% rename from fec/tests/types/badfmem.fe rename to fec/tests/types/badfld.fe index 8814d35..68704be 100644 --- a/fec/tests/types/badfmem.fe +++ b/fec/tests/types/badfld.fe @@ -1,7 +1,6 @@ -unit badfmem; +unit badfld; struct Point { x: i32, y: i32, } fn main() -> i32 { let p: Point = Point{ x: 1 }; return p.z; } - diff --git a/fec/tests/types/badidx.fe b/fec/tests/types/badindex.fe similarity index 84% rename from fec/tests/types/badidx.fe rename to fec/tests/types/badindex.fe index 19c72f7..06b1b10 100644 --- a/fec/tests/types/badidx.fe +++ b/fec/tests/types/badindex.fe @@ -1,8 +1,7 @@ -unit badidx; +unit badindex; fn main() -> i32 { let a: [2]i32 = [1, 2]; a[0] = 3; return a[0]; } - diff --git a/fec/tests/types/badmatch.fe b/fec/tests/types/badmat.fe similarity index 87% rename from fec/tests/types/badmatch.fe rename to fec/tests/types/badmat.fe index 0132224..3c7df6a 100644 --- a/fec/tests/types/badmatch.fe +++ b/fec/tests/types/badmat.fe @@ -1,7 +1,6 @@ -unit badmatch; +unit badmat; enum Shape { Empty, Circle(i32), } fn main() -> i32 { match Shape.Empty { Empty => 0; } return 0; } - diff --git a/fec/tests/types/badstrg.fe b/fec/tests/types/badstr.fe similarity index 84% rename from fec/tests/types/badstrg.fe rename to fec/tests/types/badstr.fe index 30f39e0..6219d9a 100644 --- a/fec/tests/types/badstrg.fe +++ b/fec/tests/types/badstr.fe @@ -1,7 +1,6 @@ -unit badstrg; +unit badstr; fn main() -> i32 { var text: str = "abc"; text[0] = 'z'; return 0; } - diff --git a/fec/tests/types/okarra1.fe b/fec/tests/types/ok_arra1.fe similarity index 89% rename from fec/tests/types/okarra1.fe rename to fec/tests/types/ok_arra1.fe index 8df5963..487b1ec 100644 --- a/fec/tests/types/okarra1.fe +++ b/fec/tests/types/ok_arra1.fe @@ -1,8 +1,7 @@ -unit okarra1; +unit ok_arra1; fn main() -> i32 { let bytes: [3]u8 = [1, 2, 3]; if bytes[0] == 1 and bytes[2] == 3 { return 0; } return 1; } - diff --git a/fec/tests/types/okarray.fe b/fec/tests/types/ok_array.fe similarity index 92% rename from fec/tests/types/okarray.fe rename to fec/tests/types/ok_array.fe index e11ef29..bf748bc 100644 --- a/fec/tests/types/okarray.fe +++ b/fec/tests/types/ok_array.fe @@ -1,4 +1,4 @@ -unit okarray; +unit ok_array; fn main() -> i32 { let a: [3]i32 = [1, 2, 3]; @@ -7,4 +7,3 @@ fn main() -> i32 { if a[0] + s[1] + t[0] == 5 and s.n == 3 { return 0; } return 1; } - diff --git a/fec/tests/types/okcastw.fe b/fec/tests/types/ok_castw.fe similarity index 91% rename from fec/tests/types/okcastw.fe rename to fec/tests/types/ok_castw.fe index 5d3352e..2e962f5 100644 --- a/fec/tests/types/okcastw.fe +++ b/fec/tests/types/ok_castw.fe @@ -1,4 +1,4 @@ -unit okcastw; +unit ok_castw; pub fn main() -> i32 { var x: i16 = 0; @@ -9,4 +9,3 @@ pub fn main() -> i32 { if y == 3 { return 0; } return 1; } - diff --git a/fec/tests/types/okcharc.fe b/fec/tests/types/ok_char.fe similarity index 91% rename from fec/tests/types/okcharc.fe rename to fec/tests/types/ok_char.fe index cd47d6b..e6939b7 100644 --- a/fec/tests/types/okcharc.fe +++ b/fec/tests/types/ok_char.fe @@ -1,4 +1,4 @@ -unit okcharc; +unit ok_char; fn main() -> i32 { let c: char = '\u0041'; @@ -7,4 +7,3 @@ fn main() -> i32 { if c == d and u == ('A' as u8) { return 0; } return 1; } - diff --git a/fec/tests/types/okenum.fe b/fec/tests/types/ok_enum.fe similarity index 96% rename from fec/tests/types/okenum.fe rename to fec/tests/types/ok_enum.fe index 8e6dfe4..581cbb3 100644 --- a/fec/tests/types/okenum.fe +++ b/fec/tests/types/ok_enum.fe @@ -1,4 +1,4 @@ -unit okenum; +unit ok_enum; enum Shape { Empty, Circle(i32), Rect { w: i32, h: i32, }, } @@ -14,4 +14,3 @@ fn main() -> i32 { if score(Shape.Circle(5)) == 5 and score(Shape.Rect{ w: 2, h: 3 }) == 6 { return 0; } return 1; } - diff --git a/fec/tests/types/okfor.fe b/fec/tests/types/ok_for.fe similarity index 97% rename from fec/tests/types/okfor.fe rename to fec/tests/types/ok_for.fe index d191bda..3aab7bf 100644 --- a/fec/tests/types/okfor.fe +++ b/fec/tests/types/ok_for.fe @@ -1,4 +1,4 @@ -unit okfor; +unit ok_for; fn main() -> i32 { var total: i32 = 0; @@ -16,4 +16,3 @@ fn main() -> i32 { if total == 9 and m[0] == 2 { return 0; } return 1; } - diff --git a/fec/tests/types/okhello.fe b/fec/tests/types/ok_hello.fe similarity index 95% rename from fec/tests/types/okhello.fe rename to fec/tests/types/ok_hello.fe index ed5305b..e55fedb 100644 --- a/fec/tests/types/okhello.fe +++ b/fec/tests/types/ok_hello.fe @@ -1,4 +1,4 @@ -unit okhello; +unit ok_hello; fn add(a: i32, b: i32) -> i32 { return a + b; @@ -21,4 +21,3 @@ pub fn main() -> i32 { return 1; } } - diff --git a/fec/tests/types/okmutab.fe b/fec/tests/types/ok_mutab.fe similarity index 93% rename from fec/tests/types/okmutab.fe rename to fec/tests/types/ok_mutab.fe index 8b44f6c..dc596a2 100644 --- a/fec/tests/types/okmutab.fe +++ b/fec/tests/types/ok_mutab.fe @@ -1,4 +1,4 @@ -unit okmutab; +unit ok_mutab; fn takes_shared(s: []u8) -> u8 { return s[0]; } @@ -9,4 +9,3 @@ fn main() -> i32 { if takes_shared(s) == 1 and raw[1] == 9 { return 0; } return 1; } - diff --git a/fec/tests/types/okneste.fe b/fec/tests/types/ok_neste.fe similarity index 92% rename from fec/tests/types/okneste.fe rename to fec/tests/types/ok_neste.fe index cf9c392..e8c0485 100644 --- a/fec/tests/types/okneste.fe +++ b/fec/tests/types/ok_neste.fe @@ -1,4 +1,4 @@ -unit okneste; +unit ok_neste; struct Outer { inner: Inner, } struct Inner { value: i32, } @@ -8,4 +8,3 @@ fn main() -> i32 { if x.inner.value == 7 { return 0; } return 1; } - diff --git a/fec/tests/types/okscope.fe b/fec/tests/types/ok_scope.fe similarity index 94% rename from fec/tests/types/okscope.fe rename to fec/tests/types/ok_scope.fe index 187a837..153398f 100644 --- a/fec/tests/types/okscope.fe +++ b/fec/tests/types/ok_scope.fe @@ -1,4 +1,4 @@ -unit okscope; +unit ok_scope; fn register(switch: i32) -> i32 { let auto: i32 = switch; @@ -13,4 +13,3 @@ fn register(switch: i32) -> i32 { pub fn main() -> i32 { return register(1); } - diff --git a/fec/tests/types/okstrg.fe b/fec/tests/types/ok_str.fe similarity index 89% rename from fec/tests/types/okstrg.fe rename to fec/tests/types/ok_str.fe index c758382..fec3064 100644 --- a/fec/tests/types/okstrg.fe +++ b/fec/tests/types/ok_str.fe @@ -1,8 +1,7 @@ -unit okstrg; +unit ok_str; fn main() -> i32 { let text: str = "abc"; if text[1] == ('b' as u8) and text.n == 3 { return 0; } return 1; } - diff --git a/fec/tests/types/okstruc.fe b/fec/tests/types/ok_struc.fe similarity index 96% rename from fec/tests/types/okstruc.fe rename to fec/tests/types/ok_struc.fe index ac73d35..1754eb9 100644 --- a/fec/tests/types/okstruc.fe +++ b/fec/tests/types/ok_struc.fe @@ -1,4 +1,4 @@ -unit okstruc; +unit ok_struc; struct Point { x: i32, y: i32, } packed struct PackedPoint { x: u8, y: i32, } @@ -12,4 +12,3 @@ fn main() -> i32 { (Point{ x: 1, y: 2 }.x == 1) and @size_of(Natural) == 8 { return 0; } return 1; } - From 5217c352b092150e0085dfc085c76e2ba7faf070 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:21:52 +0900 Subject: [PATCH 122/184] =?UTF-8?q?docs:=20AGENTS=EC=97=90=20=EB=82=A8?= =?UTF-8?q?=EC=9D=80=20=EC=98=9B=20=EC=B0=B8=EC=A1=B0=EB=A5=BC=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사라진 tools/README.md, SPEC.AUDIT.md, pytest, 없는 --keep-failed 플래그를 가리키고 있었다. 마일스톤 언급도 뺀다. --- AGENTS.md | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7da3312..3c8c5da 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,9 +8,10 @@ DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 | 파일 | 역할 | |---|---| | `SPEC.md` | 언어 명세. 유일한 규범 문서. 구현 지시서와 표준 라이브러리 명세는 별도 문서 | -| `tools/README.md` | 호스트 요구사항, 최초 셋업, 자동화 구조 | +| `TODO.md` | 남은 작업, 미결 결정, 순서 | +| `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 | -개발 환경과 테스트 명령 목록·플래그는 다음 CLI로 확인한다. +테스트 명령과 플래그는 다음 CLI로 확인한다. ```powershell uv run python tests/run.py --help @@ -22,12 +23,14 @@ uv run python tests/run.py --help - 코드 생성기(백엔드/IR/`lowering`)는 아직 없다. - 호스트 C 컴파일러는 구현/검증 대상이 아니다. 호스트는 편집, Git, 다운로드, 격리 작업공간 준비에만 쓴다. -- 완료하려는 기능을 직접 검사하는 `pytest` case가 통과해야 한다. 테스트가 증명하지 - 않는 기능은 완료로 처리하지 않는다. -- `uv run python tests/run.py` 는 프런트엔드 검증 엔트리이다. -- VGA 데모처럼 수동 검증이 필요한 항목은 자동 완료 게이트에서 제외한다. -- 실행 환경은 `.dosboxx/`와 `tests/run.py`로 준비되며, 과거 VM 이미지/호스트 바이너리는 - 완료 근거로 쓰지 않는다. +- 완료하려는 기능을 직접 검사하는 fixture가 통과해야 한다. 테스트가 증명하지 않는 + 기능은 완료로 처리하지 않는다. +- `uv run python tests/run.py` 가 유일한 검증 엔트리다. 툴체인은 `.dosboxx/watcom`에 + 고정되어 있고, 없으면 오류로 멈춘다. +- 거부를 기대하는 fixture는 첫 줄에 `// ERROR:<줄>:<문구>` 마커를 둔다. 마커가 없으면 + "거부되기만 하면 통과"라 검증이 약하다. +- 과거 VM 이미지나 호스트에 남은 바이너리는 완료 근거로 쓰지 않는다. +- 실행해야만 검증되는 fixture는 `fec/tests/pending-backend/`에 두고 러너가 건너뛴다. ## 빌드 함정 @@ -41,18 +44,18 @@ uv run python tests/run.py --help 명시적으로 줄인다. - [백엔드 복귀 시 유효] `R:`은 읽기 전용 저장소, `W:`은 읽기 전용 Watcom이다. 빌드 산출물은 반드시 임시 `C:\FEC`에 쓴다. -- [백엔드 복귀 시 유효] 실패 분석이 필요하면 `tests/run.py`의 `--keep-failed` 또는 - 런처 플래그로 임시 작업공간을 보존한다. +- [백엔드 복귀 시 유효] 실패 분석에는 임시 작업공간 보존이 필요하다. 그 플래그는 + 런처와 함께 사라졌으므로 다시 만들어야 한다. ## 작업 흐름 -- 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신하고 `SPEC.AUDIT.md`에 사유를 누적한다. - 구현이 명세와 다르면 둘 중 하나가 틀린 것이므로 그 자리에서 결론을 낸다. -- 검증된 마일스톤마다 커밋하고 항상 `origin`에 푸시한다. +- 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신한다. 구현이 명세와 다르면 둘 중 하나가 + 틀린 것이므로 그 자리에서 결론을 낸다. +- 코드는 컴파일러 단계로 나눈다. 마일스톤 단위 분할은 폐기했다. +- 검증된 단위마다 커밋한다. 푸시는 요청받았을 때만 한다. - primary 브랜치는 `master`다. - `.dosboxx/`의 다운로드, 실행 작업공간, 로그는 커밋하지 않는다. ## 현재 상태 -현재 구현 상태와 다음 마일스톤은 `SPEC.md`와 테스트 registry를 기준으로 판단한다. -과거 VM 이미지나 호스트에 남은 바이너리를 근거로 완료 처리하지 않는다. +`uv run python tests/run.py` 의 통과 수가 현재 상태다. 남은 작업은 `TODO.md`에 있다. From 2f140e1e48bb47cb3a48a8081f2c94cc428328c1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:30:45 +0900 Subject: [PATCH 123/184] =?UTF-8?q?fixture=20=EC=A7=84=EB=8B=A8=20?= =?UTF-8?q?=EC=A6=9D=EA=B1=B0=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fixture-report.md | 597 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 fixture-report.md diff --git a/fixture-report.md b/fixture-report.md new file mode 100644 index 0000000..0bec0ce --- /dev/null +++ b/fixture-report.md @@ -0,0 +1,597 @@ +# 마커 없는 fixture 진단 증거 보고서 + +기준선: `uv run python tests/run.py` → `150/188 passed (58 pin a line and message)` + +이 보고서는 마커가 없는 37개 fixture를 직접 읽고, 기준선에서 새로 빌드된 +`.build/fec.exe --check`의 실제 진단을 확인한 결과다. 진단 전문의 색상 제어 문자는 +가독성을 위해 제거했으며, 텍스트·줄·열·진단 순서는 그대로 기록했다. + +## types + +### `fec/tests/types/bad_ari.fe` + +- 검사 대상: 함수 호출 인자 개수 불일치. +- 근거: `add`는 두 인자를 받지만 8행에서 `add(1)`로 한 인자만 전달한다. +- 실제 진단: + + ```text + fec/tests/types/bad_ari.fe:8:15: error: wrong number of arguments + 8 | return add(1); + | ^ + ``` + +- 일치 여부: 예 — 호출의 인자 개수 위반을 직접 진단한다. + +### `fec/tests/types/bad_asgn.fe` + +- 검사 대상: 불변 `let` 변수에 대입. +- 근거: 4행에서 `let value`로 선언한 뒤 5행에서 `value = 2`로 대입한다. +- 실제 진단: + + ```text + fec/tests/types/bad_asgn.fe:5:5: error: cannot assign to immutable let + 5 | value = 2; + | ^ + ``` + +- 일치 여부: 예 — `let`의 불변성 위반을 직접 진단한다. + +### `fec/tests/types/bad_cast.fe` + +- 검사 대상: 허용되지 않는 타입의 `as` 변환. +- 근거: 4행에서 `bool` 값 `true`를 `i32`로 변환한다. +- 실제 진단: + + ```text + fec/tests/types/bad_cast.fe:4:23: error: 'as' requires integer or char types + 4 | let x: i32 = true as i32; + | ^ + ``` + +- 일치 여부: 예 — 정수/문자가 아닌 피연산자의 캐스트를 직접 진단한다. + +### `fec/tests/types/bad_cond.fe` + +- 검사 대상: 조건식의 비-`bool` 값 사용. +- 근거: 4행의 `if 1`에서 정수 리터럴을 조건으로 사용한다. +- 실제 진단: + + ```text + fec/tests/types/bad_cond.fe:4:5: error: if condition must be bool + 4 | if 1 { return 0; } + | ^ + ``` + +- 일치 여부: 예 — 조건식이 `bool`이어야 한다는 규칙을 직접 진단한다. + +### `fec/tests/types/bad_mlet.fe` + +- 검사 대상: `let`으로 mutable slice를 바인딩. +- 근거: 4행에서 mutable 배열 slice를 만든 뒤 5행의 `let s: []mut u8`에 바인딩한다. +- 실제 진단: + + ```text + fec/tests/types/bad_mlet.fe:5:5: error: let cannot bind a mutable slice + 5 | let s: []mut u8 = raw[..]; + | ^ + ``` + +- 일치 여부: 예 — mutable slice의 `let` 바인딩 금지를 직접 진단한다. + +### `fec/tests/types/bad_ret.fe` + +- 검사 대상: 반환식과 함수 반환 타입의 불일치. +- 근거: `main`은 `i32`를 반환한다고 선언했지만 4행에서 `true`를 반환한다. +- 실제 진단: + + ```text + fec/tests/types/bad_ret.fe:4:5: error: return type mismatch + 4 | return true; + | ^ + ``` + +- 일치 여부: 예 — 반환 타입 불일치를 직접 진단한다. + +### `fec/tests/types/bad_shwr.fe` + +- 검사 대상: shared slice를 통한 쓰기. +- 근거: `s`는 `[]u8` shared slice인데 4행에서 `s[0] = 1`로 쓴다. +- 실제 진단: + + ```text + fec/tests/types/bad_shwr.fe:4:6: error: cannot write through shared slice + 4 | s[0] = 1; + | ^ + ``` + +- 일치 여부: 예 — shared slice 쓰기 위반을 직접 진단한다. + +### `fec/tests/types/bad_type.fe` + +- 검사 대상: 함수 인자 타입 불일치. +- 근거: `add`의 첫 인자는 `i32`인데 8행에서 `true`를 전달한다. +- 실제 진단: + + ```text + fec/tests/types/bad_type.fe:8:16: error: argument type mismatch + 8 | return add(true, 1); + | ^ + ``` + +- 일치 여부: 예 — 함수 인자의 타입 불일치를 직접 진단한다. + +### `fec/tests/types/bad_unit.fe` + +- 검사 대상: 초기화되지 않은 지역 변수 사용. +- 근거: 4행에서 `var value: i32`만 선언하고 값을 넣지 않은 채 5행에서 반환한다. +- 실제 진단: + + ```text + fec/tests/types/bad_unit.fe:5:12: error: use of uninitialized variable + 5 | return value; + | ^ + ``` + +- 일치 여부: 예 — 초기화되지 않은 변수 사용을 직접 진단한다. + +### `fec/tests/types/bad_unk.fe` + +- 검사 대상: 정의되지 않은 이름 사용. +- 근거: 4행에서 선언되지 않은 `missing_name`을 반환한다. +- 실제 진단: + + ```text + fec/tests/types/bad_unk.fe:4:12: error: unknown name + 4 | return missing_name; + | ^ + ``` + +- 일치 여부: 예 — 미정의 이름 사용을 직접 진단한다. + +### `fec/tests/types/bad_void.fe` + +- 검사 대상: `void` 표현식을 값 변수의 초기화식으로 사용. +- 근거: 반환값이 없는 `noop()`의 결과를 8행에서 `i32` 변수에 넣는다. +- 실제 진단: + + ```text + fec/tests/types/bad_void.fe:8:5: error: initializer type mismatch + 8 | let value: i32 = noop(); + | ^ + fec/tests/types/bad_void.fe:8:5: error: void expression cannot initialize a variable + 8 | let value: i32 = noop(); + | ^ + ``` + +- 일치 여부: 예 — void 표현식의 값 초기화 사용을 직접 진단한다. + +### `fec/tests/types/badarr.fe` + +- 검사 대상: 배열 리터럴의 원소 타입 및 선언된 배열 타입 불일치. +- 근거: `[2]i32`에 세 원소를 쓰고, 둘째 원소로 `bool`인 `true`를 넣는다. +- 실제 진단: + + ```text + fec/tests/types/badarr.fe:3:25: error: array element type mismatch + 3 | let a: [2]i32 = [1, true, 3]; + | ^ + fec/tests/types/badarr.fe:3:5: error: initializer type mismatch + 3 | let a: [2]i32 = [1, true, 3]; + | ^ + ``` + +- 일치 여부: 예 — 배열 원소 타입 위반을 직접 진단하고, 선언 타입 불일치도 함께 진단한다. + +### `fec/tests/types/badchar.fe` + +- 검사 대상: 명시적 캐스트 없는 `char`와 `u8`의 대입. +- 근거: 4행에서 `char` 리터럴을 `u8` 변수에 직접 넣는다. +- 실제 진단: + + ```text + fec/tests/types/badchar.fe:4:5: error: initializer type mismatch + 4 | let u: u8 = 'A'; + | ^ + fec/tests/types/badchar.fe:5:5: error: return type mismatch + 5 | return u; + | ^ + ``` + +- 일치 여부: 예 — 첫 진단이 char/u8 직접 대입의 타입 불일치를 짚는다. 5행 진단은 연쇄 오류다. + +### `fec/tests/types/badcycle.fe` + +- 검사 대상: 값으로 연결된 재귀 구조체 타입. +- 근거: `A`가 `B`를 값으로 포함하고 `B`가 다시 `A`를 값으로 포함한다. +- 실제 진단: + + ```text + fec/tests/types/badcycle.fe:1:1: error: by-value recursive type + 1 | unit badcycle; + | ^ + ``` + +- 일치 여부: 예 — 값 기반 재귀 타입을 직접 진단한다. 위치가 선언부 첫 줄로 올라가지만 진단 종류는 정확하다. + +### `fec/tests/types/badfield.fe` + +- 검사 대상: 불변 구조체 값의 필드에 대입. +- 근거: `p`는 `let`으로 선언됐고 6행에서 `p.x = 3`을 수행한다. +- 실제 진단: + + ```text + fec/tests/types/badfield.fe:6:6: error: cannot assign through immutable value + 6 | p.x = 3; + | ^ + ``` + +- 일치 여부: 예 — 불변 값의 projection을 통한 쓰기를 직접 진단한다. + +### `fec/tests/types/badfld.fe` + +- 검사 대상: 구조체 초기화 필드 누락과 존재하지 않는 필드 접근. +- 근거: `Point`는 `x`, `y`를 요구하지만 4행 초기화에는 `x`만 있고, 5행에서 없는 `z`에 접근한다. +- 실제 진단: + + ```text + fec/tests/types/badfld.fe:4:20: error: missing struct field + 4 | let p: Point = Point{ x: 1 }; + | ^ + fec/tests/types/badfld.fe:5:13: error: unknown struct field + 5 | return p.z; + | ^ + ``` + +- 일치 여부: 예 — 두 필드 규칙 위반을 모두 직접 진단한다. + +### `fec/tests/types/badindex.fe` + +- 검사 대상: 불변 배열을 통한 요소 쓰기. +- 근거: `a`는 `let` 배열인데 5행에서 `a[0] = 3`을 수행한다. +- 실제 진단: + + ```text + fec/tests/types/badindex.fe:5:6: error: cannot assign through immutable value + 5 | a[0] = 3; + | ^ + ``` + +- 일치 여부: 예 — 불변 배열 index projection을 통한 쓰기를 직접 진단한다. + +### `fec/tests/types/badmat.fe` + +- 검사 대상: 비-완전 `match`. +- 근거: `Shape`에는 `Empty`, `Circle` 두 variant가 있는데 4행 match에는 `Empty`만 있다. +- 실제 진단: + + ```text + fec/tests/types/badmat.fe:4:5: error: non-exhaustive match + 4 | match Shape.Empty { Empty => 0; } + | ^ + ``` + +- 일치 여부: 예 — match의 비-완전성을 직접 진단한다. + +### `fec/tests/types/badstr.fe` + +- 검사 대상: shared string/slice를 통한 쓰기. +- 근거: `str`인 `text`의 4행에서 인덱스 요소에 대입한다. +- 실제 진단: + + ```text + fec/tests/types/badstr.fe:4:9: error: cannot write through shared slice + 4 | text[0] = 'z'; + | ^ + fec/tests/types/badstr.fe:4:13: error: assignment type mismatch + 4 | text[0] = 'z'; + | ^ + ``` + +- 일치 여부: 예 — 첫 진단이 shared string 쓰기를 직접 짚고, 두 번째는 요소 타입의 연쇄 진단이다. + +## format + +### `fec/tests/format/bad_ari.fe` + +- 검사 대상: format placeholder와 인자 개수 불일치. +- 근거: 4행의 format 문자열에는 `{}`가 두 개지만 인자는 `1` 하나다. +- 실제 진단: + + ```text + fec/tests/format/bad_ari.fe:4:6: error: format argument count mismatch + 4 | @print("{} {}", 1); + | ^ + fec/tests/format/bad_ari.fe:4:6: error: format argument count mismatch + 4 | @print("{} {}", 1); + | ^ + ``` + +- 일치 여부: 예 — 인자 개수 불일치를 직접 진단한다. 동일 진단이 중복 출력된다. + +### `fec/tests/format/bad_bufw.fe` + +- 검사 대상: `io.buf_writer(buf)`를 통한 buffer writer 구성. +- 근거: `[]mut u8` 버퍼를 `io.buf_writer`에 전달하지만, 이 호출이 정확히 어떤 금지 규칙을 의도하는지는 파일만으로 확정하기 어렵다. 명세의 `io.Writer`는 enum handle이며 `io.buf_writer` API는 정의되어 있지 않다. +- 실제 진단: + + ```text + fec/tests/format/bad_bufw.fe:6:13: error: unknown name + 6 | let w = io.buf_writer(buf); + | ^ + fec/tests/format/bad_bufw.fe:6:26: error: invalid enum variant constructor + 6 | let w = io.buf_writer(buf); + | ^ + ``` + +- 일치 여부: 애매 — 존재하지 않는 `io.buf_writer`를 거부한다는 점은 맞지만, fixture가 검사하려는 구체적인 buffer-writer 규칙을 진단한 것인지 코드만으로 판정할 수 없다. + +### `fec/tests/format/bad_cls.fe` + +- 검사 대상: 닫히지 않은 placeholder가 아니라 unmatched `}` 형식 오류. +- 근거: 4행의 format 문자열이 단독 `}`를 포함한다. +- 실제 진단: + + ```text + fec/tests/format/bad_cls.fe:4:6: error: unmatched '}' in format + 4 | @print("}", 1); + | ^ + fec/tests/format/bad_cls.fe:4:6: error: format argument count mismatch + 4 | @print("}", 1); + | ^ + ``` + +- 일치 여부: 예 — 첫 진단이 unmatched `}`를 직접 짚는다. 두 번째는 파생된 개수 진단이다. + +### `fec/tests/format/bad_many.fe` + +- 검사 대상: placeholder보다 많은 format 인자. +- 근거: 문자열에는 `{}` 하나뿐인데 4행에서 `1, 2` 두 인자를 전달한다. +- 실제 진단: + + ```text + fec/tests/format/bad_many.fe:4:6: error: format argument count mismatch + 4 | @print("{}", 1, 2); + | ^ + ``` + +- 일치 여부: 예 — format 인자 개수 불일치를 직접 진단한다. + +### `fec/tests/format/bad_open.fe` + +- 검사 대상: 닫히지 않은 format placeholder. +- 근거: 4행의 문자열에 여는 `{`만 있고 닫는 `}`가 없다. +- 실제 진단: + + ```text + fec/tests/format/bad_open.fe:4:6: error: unterminated format placeholder + 4 | @print("{", 1); + | ^ + fec/tests/format/bad_open.fe:4:6: error: format argument count mismatch + 4 | @print("{", 1); + | ^ + ``` + +- 일치 여부: 예 — 첫 진단이 종료되지 않은 placeholder를 직접 짚는다. 두 번째는 파생된 개수 진단이다. + +### `fec/tests/format/bad_run.fe` + +- 검사 대상: 런타임 문자열을 format 문자열로 사용. +- 근거: 4행에서 `fmt`를 `var str`로 선언하고 5행에서 `@print(fmt, 1)`에 전달한다. +- 실제 진단: + + ```text + fec/tests/format/bad_run.fe:5:6: error: format must be a comptime string + 5 | @print(fmt, 1); + | ^ + ``` + +- 일치 여부: 예 — format 문자열의 comptime 제약을 직접 진단한다. + +### `fec/tests/format/bad_try.fe` + +- 검사 대상: 오류 결과가 아닌 `@print`에 `try` 사용. +- 근거: 명세상 `@print`은 `void`를 반환하는데 4행에서 `try @print(...)`을 쓴다. +- 실제 진단: + + ```text + fec/tests/format/bad_try.fe:4:5: error: try requires an error result + 4 | try @print("nope"); + | ^ + ``` + +- 일치 여부: 예 — `try`의 오류 결과 요구를 직접 진단한다. + +### `fec/tests/format/bad_type.fe` + +- 검사 대상: format writer가 없는 타입을 format 인자로 사용. +- 근거: `Point` 구조체 값을 7행에서 `{}` placeholder의 인자로 전달한다. +- 실제 진단: + + ```text + fec/tests/format/bad_type.fe:7:18: error: no fmt writer for argument type + 7 | @print("{}", p); + | ^ + ``` + +- 일치 여부: 예 — 해당 타입을 포맷할 writer가 없음을 직접 진단한다. + +### `fec/tests/format/bad_verb.fe` + +- 검사 대상: 지원되지 않는 format verb. +- 근거: 4행의 `{q}`에서 `q`는 명세에 없는 verb다. +- 실제 진단: + + ```text + fec/tests/format/bad_verb.fe:4:6: error: unsupported format verb + 4 | @print("{q}", 1); + | ^ + ``` + +- 일치 여부: 예 — 지원되지 않는 verb를 직접 진단한다. + +### `fec/tests/format/bad_writ.fe` + +- 검사 대상: `@fprint` 첫 인자의 `io.Writer` 타입 위반. +- 근거: 5행에서 writer 대신 `i32` 변수 `x`를 첫 인자로 전달한다. +- 실제 진단: + + ```text + fec/tests/format/bad_writ.fe:5:13: error: @fprint requires io.Writer + 5 | @fprint(x, "bad"); + | ^ + ``` + +- 일치 여부: 예 — `@fprint`의 writer 요구를 직접 진단한다. + +## own + +### `fec/tests/own/bad_clos.fe` + +- 검사 대상: 소유 값을 close 후 다시 사용. +- 근거: 11행의 `try file.close()`가 `file`을 이동시키고 12행에서 다시 `file.close()`를 호출한다. +- 실제 진단: + + ```text + fec/tests/own/bad_clos.fe:12:5: error: use of moved value + 12 | file.close(); + | ^ + fec/tests/own/bad_clos.fe:11:9: note: value was moved here + 11 | try file.close(); + | ^ + fec/tests/own/bad_clos.fe:12:5: error: use of moved value + 12 | file.close(); + | ^ + fec/tests/own/bad_clos.fe:11:9: note: value was moved here + 11 | try file.close(); + | ^ + ``` + +- 일치 여부: 예 — 이동된 값을 재사용한 위치와 이동 위치를 직접 진단한다. 동일 진단이 중복 출력된다. + +### `fec/tests/own/bad_cond.fe` + +- 검사 대상: 조건부 이동 후 값의 무조건 사용. +- 근거: 6행의 조건 분기 안에서 `take(p)`가 `p`를 이동할 수 있고 7행에서 무조건 `p`를 쓴다. +- 실제 진단: + + ```text + fec/tests/own/bad_cond.fe:7:5: error: use of possibly moved value + 7 | p.^ = 3; + | ^ + fec/tests/own/bad_cond.fe:7:5: error: use of possibly moved value + 7 | p.^ = 3; + | ^ + ``` + +- 일치 여부: 예 — 조건부 이동 가능성을 직접 진단한다. 동일 진단이 중복 출력된다. + +### `fec/tests/own/bad_dbl.fe` + +- 검사 대상: 소유 포인터의 이중 destroy. +- 근거: 4행에서 `p`를 destroy한 뒤 5행에서 같은 `p`를 다시 destroy한다. +- 실제 진단: + + ```text + fec/tests/own/bad_dbl.fe:5:17: error: use of moved value + 5 | mem.destroy(p); + | ^ + fec/tests/own/bad_dbl.fe:4:17: note: value was moved here + 4 | mem.destroy(p); + | ^ + fec/tests/own/bad_dbl.fe:5:17: error: use of moved value + 5 | mem.destroy(p); + | ^ + fec/tests/own/bad_dbl.fe:4:17: note: value was moved here + 4 | mem.destroy(p); + | ^ + ``` + +- 일치 여부: 예 — 두 번째 destroy의 이동 후 사용을 직접 진단한다. 동일 진단이 중복 출력된다. + +### `fec/tests/own/bad_dest.fe` + +- 검사 대상: owned pointer가 아닌 값을 `mem.destroy`에 전달. +- 근거: 4행에서 일반 `i32` 값 `x`를 `mem.destroy(x)`에 전달한다. +- 실제 진단: + + ```text + fec/tests/own/bad_dest.fe:4:16: error: mem.destroy requires exactly one owned pointer + 4 | mem.destroy(x); + | ^ + ``` + +- 일치 여부: 예 — `mem.destroy`의 owned pointer 요구를 직접 진단한다. + +### `fec/tests/own/bad_drop.fe` + +- 검사 대상: 사용자 `drop` 메서드의 직접 호출. +- 근거: `Box`에 `drop`을 정의했지만 10행에서 `b.drop()`으로 직접 호출한다. +- 실제 진단: + + ```text + fec/tests/own/bad_drop.fe:10:11: error: drop may only be invoked by scope cleanup + 10 | b.drop(); + | ^ + ``` + +- 일치 여부: 예 — drop은 scope cleanup에서만 호출된다는 규칙을 직접 진단한다. + +### `fec/tests/own/bad_loop.fe` + +- 검사 대상: 반복문 안의 이동으로 인한 possibly-moved 값 사용. +- 근거: 6행의 `while` 본문에서 매 반복 `take(p)`가 `p`를 이동할 수 있다. +- 실제 진단: + + ```text + fec/tests/own/bad_loop.fe:6:24: error: use of possibly moved value + 6 | while again { take(p); } + | ^ + fec/tests/own/bad_loop.fe:6:24: error: use of possibly moved value + 6 | while again { take(p); } + | ^ + ``` + +- 일치 여부: 예 — 반복에 따른 possibly-moved 상태를 직접 진단한다. 동일 진단이 중복 출력된다. + +### `fec/tests/own/bad_move.fe` + +- 검사 대상: 함수 인자의 이중 이동. +- 근거: 6행의 첫 `take(p)`가 `p`를 이동한 뒤 7행에서 다시 `take(p)`를 호출한다. +- 실제 진단: + + ```text + fec/tests/own/bad_move.fe:7:10: error: use of moved value + 7 | take(p); + | ^ + fec/tests/own/bad_move.fe:6:10: note: value was moved here + 6 | take(p); + | ^ + fec/tests/own/bad_move.fe:7:10: error: use of moved value + 7 | take(p); + | ^ + fec/tests/own/bad_move.fe:6:10: note: value was moved here + 6 | take(p); + | ^ + ``` + +- 일치 여부: 예 — 이동된 인자의 재사용과 최초 이동 위치를 직접 진단한다. 동일 진단이 중복 출력된다. + +### `fec/tests/own/bad_proj.fe` + +- 검사 대상: 구조체 projection에서 non-Copy 소유 필드 이동. +- 근거: `Holder.p`는 owned pointer이고 7행에서 `take(h.p)`로 필드 밖으로 직접 이동하려 한다. +- 실제 진단: + + ```text + fec/tests/own/bad_proj.fe:7:11: error: cannot move a non-Copy value out of a projection; use mem.replace + 7 | take(h.p); + | ^ + ``` + +- 일치 여부: 예 — projection에서 non-Copy 값을 이동할 수 없다는 규칙과 대안을 직접 진단한다. + +## 판정 요약 + +- `아니오`: 없음. +- `애매`: `fec/tests/format/bad_bufw.fe` — `io.buf_writer` 자체가 명세에 없으므로 fixture의 구체적 의도를 확정할 수 없음. +- 나머지 36개: 실제 진단이 코드가 검사하려는 규칙과 일치. From c900e0f61c6199198a82f24e457a4b8e3c6c815e Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:39:58 +0900 Subject: [PATCH 124/184] =?UTF-8?q?spec:=20640KB=20=EC=85=80=ED=94=84?= =?UTF-8?q?=ED=98=B8=EC=8A=A4=ED=8C=85=EC=9D=84=20=EB=AA=A9=ED=91=9C?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=82=B4=EB=A6=B0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 철학 2의 근거가 사실과 달랐다. 전역 분석 금지만으로는 메모리가 줄지 않고, 현재 프론트엔드는 이미 그 예산을 한 자릿수 넘겼다 — FeBuild 하나가 26,892바이트 스택 지역 변수이고, 유닛 64개의 소스와 AST를 동시에 들고 있다. 전역 분석 금지는 국소적 진단과 작은 컴파일러라는 자체 근거로 유지한다. 컴파일러가 도는 곳을 §2.1로 분리했다. bits16 타깃은 그대로다 -- 8086용 프로그램을 만드는 것과 8086에서 컴파일러를 돌리는 것은 다른 일이다. --- SPEC.md | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/SPEC.md b/SPEC.md index 8eb5444..a80ca2a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -11,7 +11,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 ## 1. 설계 철학 1. **안전은 기본, 위험은 명시.** 기본 코드는 메모리 안전(널 역참조, 버퍼 오버런, use-after-free, 이중 해제 불가). 위험한 연산은 `unsafe {}` 블록 안에서만. -2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 640KB 머신에서 셀프호스팅을 가능하게 한다. +2. **전역 분석 금지.** 모든 검사(타입, 소유권, 참조)는 함수 하나만 보고 완결되어야 한다. 이 제약이 라이프타임 표기를 없애고, 진단을 위반 지점에 국소적으로 묶으며, 컴파일러를 작게 유지한다. 3. **숨은 비용 없음.** 힙 할당, 복사, 소멸자 호출, 형변환이 전부 소스에 보인다. GC 없음, 예외 없음, 암묵 변환 없음. 4. **읽히는 문법.** `이름: 타입` 순서, 좌→우 파싱, LL(1) 재귀하강으로 처리 가능. 5. **작게 시작.** 기능을 넣기 전에 뺄 이유를 먼저 찾는다. 뺀 것과 그 대체 수단은 §11에 기록한다. @@ -32,6 +32,20 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 - 소스 분기: `comptime if @bits == 16 { ... } else { ... }` - `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. +### 2.1 컴파일러 자신이 도는 곳 + +위 표는 **생성되는 프로그램**의 타깃이다. 컴파일러 `fec`이 도는 곳은 별개이며 32비트 +보호모드 플랫이다 — 호스트에서든, DOS에서든 DPMI 익스텐더 위에서다. 당대의 Open +Watcom 컴파일러 자신이 그렇게 돌았다. + +**8086 리얼모드에서 `fec`을 돌리는 것은 목표가 아니다.** 640KB는 8086의 한계가 아니라 +IBM PC가 1MiB 주소 공간의 위쪽 384KB를 하드웨어에 예약해서 생긴 것이고, 그 안에 +컴파일러를 넣으려면 AST를 통째로 들지 않는 스트리밍 구조와 오버레이가 필요하다. +그것은 언어 설계가 아니라 컴파일러 구현 전략의 문제이므로 언어 명세에서 다루지 않는다. + +`bits16` 타깃은 유지된다. **8086용 프로그램을 만드는 것과 8086에서 컴파일러를 돌리는 +것은 다른 일이고, 전자만 명세의 약속이다.** + --- ## 3. 어휘 구조 @@ -757,7 +771,7 @@ binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다 **등급 정의** - `영구` — §1 철학과 정면 충돌. v2.0에서도 넣지 않는다. -- `구조적 불가` — 넣으면 R4를 풀어야 하고 전역 분석이 생겨 셀프호스팅 목표가 깨진다. 이 언어의 정의상 불가. +- `구조적 불가` — 넣으면 R4를 풀어야 하고 전역 분석이 생긴다(§1 철학 2). 이 언어의 정의상 불가. - `v0.2` — 넣을 예정. 순서 문제일 뿐 원칙 위반 아님. - `편의` — 원칙 위반 없음, 구현도 쉬움. 여유 생기면 아무 때나. From f5bffec7dd065d97b958c3647c7e59e4b836a749 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:49:05 +0900 Subject: [PATCH 125/184] =?UTF-8?q?fix:=20=EA=B0=99=EC=9D=80=20=EC=9C=84?= =?UTF-8?q?=EB=B0=98=EC=9D=84=20=EB=91=90=20=EB=B2=88=20=EB=B3=B4=EA=B3=A0?= =?UTF-8?q?=ED=95=98=EB=8D=98=20=EB=91=90=20=EA=B2=BD=EB=A1=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=95=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이동한 값을 쓰면 진단이 두 번 나왔다. 원인이 둘이다. 식별자를 읽으면 FE_OWN_READ 가 이미 보고하는데 mark_moved 가 FE_OWN_MOVE 로 같은 자리를 다시 보고했고, member lvalue 는 check_lvalue 가 base 를 검사한 뒤 check_lvalue_core 가 또 검사했다. M6/M7 두 검사기를 합칠 때 남은 자국이다. 러너는 진단의 첫 줄만 마커와 대조하므로 fixture 188개가 이것을 잡지 못했다. --- fec/src/check.c | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index 37e4370..031a415 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -84,6 +84,11 @@ static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) "cannot move a non-Copy value out of a projection; use mem.replace"); return; } + /* Reaching an identifier already ran FE_OWN_READ over it, and that + read reported the value as gone if it was. Running the move as well + reports the same sentence at the same column a second time, so stop + at the state the read left behind. */ + if (sym->own.move != FE_OWN_AVAILABLE) return; if (fe_own_access(s->c->diags,&sym->own,FE_OWN_MOVE,n->loc)) { sym->moved=sym->own.move; /* Keep the existing emitter contract: ownership-consuming AST @@ -304,7 +309,8 @@ static void check_match(FeCheckerState *s, FeNode *n); static void check_stmt(FeCheckerState *s, FeNode *n); static FeType *check_expr_core(FeCheckerState *s, FeNode *n); static void check_stmt_core(FeCheckerState *s, FeNode *n); -static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read); +static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, + FeType *base_in); static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read); static FeType *check_call(FeCheckerState *s, FeNode *n); @@ -1163,7 +1169,11 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) return unknown(c); } -static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read) +/* `base_in` is the already-checked type of a member expression's base. The M7 + lvalue path looks at that base before delegating here, and checking it a + second time reports any ownership violation on it a second time too. */ +static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, + FeType *base_in) { FeSym *sym; FeType *base; @@ -1189,7 +1199,7 @@ static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read) return sym->type; } if (n && n->kind == FE_N_MEMBER) { - base=check_expr(s,n->a); + base=base_in ? base_in : check_expr(s,n->a); if (base && base->kind == FE_TYPE_REF && n->b && n->b->text && strcmp(n->b->text,"^")==0) { if (!base->ref_mut) @@ -2223,7 +2233,7 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) { - FeType *base; + FeType *base=0; FeFieldType *field; FeType *owner; if (!n) return unknown(s->c); @@ -2262,7 +2272,7 @@ static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) return field->type; } } - return check_lvalue_core(s,n,read); + return check_lvalue_core(s,n,read,n->kind==FE_N_MEMBER ? base : 0); } static FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, From c7d0a900bda5fbc9368d668142a6f53ebed0b8ea Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:51:19 +0900 Subject: [PATCH 126/184] =?UTF-8?q?fix:=20=EB=82=A8=EC=9D=80=20=EC=A4=91?= =?UTF-8?q?=EB=B3=B5=20=EC=A7=84=EB=8B=A8=20=EB=91=90=20=EA=B1=B4=EC=9D=84?= =?UTF-8?q?=20=EC=A0=95=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit format 검사는 인자가 떨어진 자리에서 개수 불일치를 말하고 문자열을 다 훑은 뒤 같은 말을 또 했다. aggregate storage 검사는 M7 쪽이 optional 뒤의 참조를 보려고 도는 김에 평범한 &T 필드까지 잡아서, 뒤이어 도는 M6 검사와 겹쳤다. fixture 전수 검사 결과 --check 경로에 중복 진단이 남아 있지 않다. --- fec/src/check.c | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index 031a415..3983a75 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -659,6 +659,7 @@ static void check_format_call(FeCheckerState *s, FeNode *n) unsigned offset=0; int verb; int bad=0; + int counted=0; if (strcmp(n->text,"@fprint")==0) offset=1; fmt_node=n->children; if (offset) { @@ -693,7 +694,10 @@ static void check_format_call(FeCheckerState *s, FeNode *n) if (verb!=' ' && verb!='x' && verb!='c' && verb!='s' && verb!='b') { err(s->c,n->loc,"unsupported format verb"); bad=1; } - if (!arg) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } + if (!arg) { + err(s->c,n->loc,"format argument count mismatch"); + bad=1; counted=1; + } else { t=arg->sem_type; if (verb==' ' && t && t->kind==FE_TYPE_ENUM && t->is_error) verb='s'; @@ -705,7 +709,9 @@ static void check_format_call(FeCheckerState *s, FeNode *n) if (fmt[i]=='}') { err(s->c,n->loc,"unmatched '}' in format"); bad=1; } ++i; } - if (count!=argc) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } + /* Running out of arguments mid-string already said this. Saying it again + once the whole string has been walked adds nothing. */ + if (count!=argc && !counted) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } (void)bad; } @@ -2730,8 +2736,13 @@ static void m7_check_storage(FeCheck *c, FeNode *decl) FeNode *m; if (!decl) return; if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + /* This pass exists for the shapes the other one cannot see, such as a + reference behind an optional. A plain `&T` field is seen by both, so + leave that one to check_reference_storage below. */ for (m=decl->children;m;m=m->next) - if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a)) + if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a) && + !own_ast_reference_type(m->a) && + !own_ast_pointer_to_reference(m->a)) err(c,m->loc,"reference type is not allowed in aggregate storage"); } check_reference_storage(c,decl); From 1be47fa11cc8927542f95f4e3d33c574e9dbbb27 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:51:58 +0900 Subject: [PATCH 127/184] =?UTF-8?q?tests:=20=EB=A7=88=EC=BB=A4=20=EC=97=86?= =?UTF-8?q?=EB=8D=98=20fixture=2036=EA=B0=9C=EC=97=90=20=EC=A7=84=EB=8B=A8?= =?UTF-8?q?=EC=9D=84=20=EA=B3=A0=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 마커가 없으면 러너는 '거부되기만 하면 통과'로 판정한다. 엉뚱한 이유로 거부돼도 초록이었다. fixture 별로 무엇을 검사하는지 읽고 지금 나오는 진단이 그 규칙을 짚는지 확인한 뒤 줄과 문구를 고정했다. 근거는 fixture-report.md에 있다. pin 58 -> 94. 통과 수는 150/188 그대로다. --- fec/tests/format/bad_ari.fe | 1 + fec/tests/format/bad_cls.fe | 1 + fec/tests/format/bad_many.fe | 1 + fec/tests/format/bad_open.fe | 1 + fec/tests/format/bad_run.fe | 1 + fec/tests/format/bad_try.fe | 1 + fec/tests/format/bad_type.fe | 1 + fec/tests/format/bad_verb.fe | 1 + fec/tests/format/bad_writ.fe | 1 + fec/tests/own/bad_clos.fe | 1 + fec/tests/own/bad_cond.fe | 1 + fec/tests/own/bad_dbl.fe | 1 + fec/tests/own/bad_dest.fe | 1 + fec/tests/own/bad_drop.fe | 1 + fec/tests/own/bad_loop.fe | 1 + fec/tests/own/bad_move.fe | 1 + fec/tests/own/bad_proj.fe | 1 + fec/tests/types/bad_ari.fe | 1 + fec/tests/types/bad_asgn.fe | 1 + fec/tests/types/bad_cast.fe | 1 + fec/tests/types/bad_cond.fe | 1 + fec/tests/types/bad_mlet.fe | 1 + fec/tests/types/bad_ret.fe | 1 + fec/tests/types/bad_shwr.fe | 1 + fec/tests/types/bad_type.fe | 1 + fec/tests/types/bad_unit.fe | 1 + fec/tests/types/bad_unk.fe | 1 + fec/tests/types/bad_void.fe | 1 + fec/tests/types/badarr.fe | 1 + fec/tests/types/badchar.fe | 1 + fec/tests/types/badcycle.fe | 1 + fec/tests/types/badfield.fe | 1 + fec/tests/types/badfld.fe | 1 + fec/tests/types/badindex.fe | 1 + fec/tests/types/badmat.fe | 1 + fec/tests/types/badstr.fe | 1 + 36 files changed, 36 insertions(+) diff --git a/fec/tests/format/bad_ari.fe b/fec/tests/format/bad_ari.fe index 7dc30b8..4dde572 100644 --- a/fec/tests/format/bad_ari.fe +++ b/fec/tests/format/bad_ari.fe @@ -1,3 +1,4 @@ +// ERROR:5:format argument count mismatch unit bad_ari; fn main() -> i32 { diff --git a/fec/tests/format/bad_cls.fe b/fec/tests/format/bad_cls.fe index 1f8a720..0871f16 100644 --- a/fec/tests/format/bad_cls.fe +++ b/fec/tests/format/bad_cls.fe @@ -1,3 +1,4 @@ +// ERROR:5:unmatched '}' in format unit bad_cls; fn main() -> i32 { diff --git a/fec/tests/format/bad_many.fe b/fec/tests/format/bad_many.fe index ab25401..d7608d6 100644 --- a/fec/tests/format/bad_many.fe +++ b/fec/tests/format/bad_many.fe @@ -1,3 +1,4 @@ +// ERROR:5:format argument count mismatch unit bad_many; fn main() -> i32 { diff --git a/fec/tests/format/bad_open.fe b/fec/tests/format/bad_open.fe index 98ee2c8..00a59f2 100644 --- a/fec/tests/format/bad_open.fe +++ b/fec/tests/format/bad_open.fe @@ -1,3 +1,4 @@ +// ERROR:5:unterminated format placeholder unit bad_open; fn main() -> i32 { diff --git a/fec/tests/format/bad_run.fe b/fec/tests/format/bad_run.fe index c7945e1..d5d81b7 100644 --- a/fec/tests/format/bad_run.fe +++ b/fec/tests/format/bad_run.fe @@ -1,3 +1,4 @@ +// ERROR:6:format must be a comptime string unit bad_run; fn main() -> i32 { diff --git a/fec/tests/format/bad_try.fe b/fec/tests/format/bad_try.fe index 0c5da62..b09ecea 100644 --- a/fec/tests/format/bad_try.fe +++ b/fec/tests/format/bad_try.fe @@ -1,3 +1,4 @@ +// ERROR:5:try requires an error result unit bad_try; fn main() -> i32 { diff --git a/fec/tests/format/bad_type.fe b/fec/tests/format/bad_type.fe index 9279e47..1b9ff35 100644 --- a/fec/tests/format/bad_type.fe +++ b/fec/tests/format/bad_type.fe @@ -1,3 +1,4 @@ +// ERROR:8:no fmt writer for argument type unit bad_type; struct Point { x: i32, } diff --git a/fec/tests/format/bad_verb.fe b/fec/tests/format/bad_verb.fe index 9a928ce..e683f90 100644 --- a/fec/tests/format/bad_verb.fe +++ b/fec/tests/format/bad_verb.fe @@ -1,3 +1,4 @@ +// ERROR:5:unsupported format verb unit bad_verb; fn main() -> i32 { diff --git a/fec/tests/format/bad_writ.fe b/fec/tests/format/bad_writ.fe index 18e33a6..6331833 100644 --- a/fec/tests/format/bad_writ.fe +++ b/fec/tests/format/bad_writ.fe @@ -1,3 +1,4 @@ +// ERROR:6:@fprint requires io.Writer unit bad_writ; fn main() -> i32 { diff --git a/fec/tests/own/bad_clos.fe b/fec/tests/own/bad_clos.fe index 28ff3e4..298b0d5 100644 --- a/fec/tests/own/bad_clos.fe +++ b/fec/tests/own/bad_clos.fe @@ -1,3 +1,4 @@ +// ERROR:13:use of moved value unit bad_clos; struct FileLike { diff --git a/fec/tests/own/bad_cond.fe b/fec/tests/own/bad_cond.fe index f46cfe4..a869374 100644 --- a/fec/tests/own/bad_cond.fe +++ b/fec/tests/own/bad_cond.fe @@ -1,3 +1,4 @@ +// ERROR:8:use of possibly moved value unit bad_cond; fn take(p: ^i32) -> void { mem.destroy(p); } diff --git a/fec/tests/own/bad_dbl.fe b/fec/tests/own/bad_dbl.fe index a990673..52a7cca 100644 --- a/fec/tests/own/bad_dbl.fe +++ b/fec/tests/own/bad_dbl.fe @@ -1,3 +1,4 @@ +// ERROR:6:use of moved value unit bad_dbl; fn bad(p: ^i32) -> void { diff --git a/fec/tests/own/bad_dest.fe b/fec/tests/own/bad_dest.fe index 8cedd19..3db184a 100644 --- a/fec/tests/own/bad_dest.fe +++ b/fec/tests/own/bad_dest.fe @@ -1,3 +1,4 @@ +// ERROR:5:mem.destroy requires exactly one owned pointer unit bad_dest; fn bad(x: i32) -> void { diff --git a/fec/tests/own/bad_drop.fe b/fec/tests/own/bad_drop.fe index ad9e9cc..a80a0a8 100644 --- a/fec/tests/own/bad_drop.fe +++ b/fec/tests/own/bad_drop.fe @@ -1,3 +1,4 @@ +// ERROR:11:drop may only be invoked by scope cleanup unit bad_drop; struct Box { diff --git a/fec/tests/own/bad_loop.fe b/fec/tests/own/bad_loop.fe index c7fd822..3f74193 100644 --- a/fec/tests/own/bad_loop.fe +++ b/fec/tests/own/bad_loop.fe @@ -1,3 +1,4 @@ +// ERROR:7:use of possibly moved value unit bad_loop; fn take(p: ^i32) -> void { mem.destroy(p); } diff --git a/fec/tests/own/bad_move.fe b/fec/tests/own/bad_move.fe index 15d25fb..01496ba 100644 --- a/fec/tests/own/bad_move.fe +++ b/fec/tests/own/bad_move.fe @@ -1,3 +1,4 @@ +// ERROR:8:use of moved value unit bad_move; fn take(p: ^i32) -> void { mem.destroy(p); } diff --git a/fec/tests/own/bad_proj.fe b/fec/tests/own/bad_proj.fe index 0c50987..ba5fd64 100644 --- a/fec/tests/own/bad_proj.fe +++ b/fec/tests/own/bad_proj.fe @@ -1,3 +1,4 @@ +// ERROR:8:cannot move a non-Copy value out of a projection; use mem.replace unit bad_proj; struct Holder { p: ^i32 } diff --git a/fec/tests/types/bad_ari.fe b/fec/tests/types/bad_ari.fe index bdf4619..4a26425 100644 --- a/fec/tests/types/bad_ari.fe +++ b/fec/tests/types/bad_ari.fe @@ -1,3 +1,4 @@ +// ERROR:9:wrong number of arguments unit bad_ari; fn add(a: i32, b: i32) -> i32 { diff --git a/fec/tests/types/bad_asgn.fe b/fec/tests/types/bad_asgn.fe index 3e921bc..8171754 100644 --- a/fec/tests/types/bad_asgn.fe +++ b/fec/tests/types/bad_asgn.fe @@ -1,3 +1,4 @@ +// ERROR:6:cannot assign to immutable let unit bad_asgn; fn main() -> i32 { diff --git a/fec/tests/types/bad_cast.fe b/fec/tests/types/bad_cast.fe index 0eafcfc..5881636 100644 --- a/fec/tests/types/bad_cast.fe +++ b/fec/tests/types/bad_cast.fe @@ -1,3 +1,4 @@ +// ERROR:5:'as' requires integer or char types unit bad_cast; fn main() -> i32 { diff --git a/fec/tests/types/bad_cond.fe b/fec/tests/types/bad_cond.fe index a3968f1..72e2347 100644 --- a/fec/tests/types/bad_cond.fe +++ b/fec/tests/types/bad_cond.fe @@ -1,3 +1,4 @@ +// ERROR:5:if condition must be bool unit bad_cond; fn main() -> i32 { diff --git a/fec/tests/types/bad_mlet.fe b/fec/tests/types/bad_mlet.fe index a3960b0..7211577 100644 --- a/fec/tests/types/bad_mlet.fe +++ b/fec/tests/types/bad_mlet.fe @@ -1,3 +1,4 @@ +// ERROR:6:let cannot bind a mutable slice unit bad_mlet; fn bad() -> void { diff --git a/fec/tests/types/bad_ret.fe b/fec/tests/types/bad_ret.fe index a151ab3..367a1f5 100644 --- a/fec/tests/types/bad_ret.fe +++ b/fec/tests/types/bad_ret.fe @@ -1,3 +1,4 @@ +// ERROR:5:return type mismatch unit bad_ret; fn main() -> i32 { diff --git a/fec/tests/types/bad_shwr.fe b/fec/tests/types/bad_shwr.fe index 7c7881f..c67fc29 100644 --- a/fec/tests/types/bad_shwr.fe +++ b/fec/tests/types/bad_shwr.fe @@ -1,3 +1,4 @@ +// ERROR:5:cannot write through shared slice unit bad_shwr; fn bad(s: []u8) -> void { diff --git a/fec/tests/types/bad_type.fe b/fec/tests/types/bad_type.fe index 15a4aca..ba421b9 100644 --- a/fec/tests/types/bad_type.fe +++ b/fec/tests/types/bad_type.fe @@ -1,3 +1,4 @@ +// ERROR:9:argument type mismatch unit bad_type; fn add(a: i32, b: i32) -> i32 { diff --git a/fec/tests/types/bad_unit.fe b/fec/tests/types/bad_unit.fe index 99d8bbd..be33f54 100644 --- a/fec/tests/types/bad_unit.fe +++ b/fec/tests/types/bad_unit.fe @@ -1,3 +1,4 @@ +// ERROR:6:use of uninitialized variable unit bad_unit; fn main() -> i32 { diff --git a/fec/tests/types/bad_unk.fe b/fec/tests/types/bad_unk.fe index b868c3b..12ab2c4 100644 --- a/fec/tests/types/bad_unk.fe +++ b/fec/tests/types/bad_unk.fe @@ -1,3 +1,4 @@ +// ERROR:5:unknown name unit bad_unk; fn main() -> i32 { diff --git a/fec/tests/types/bad_void.fe b/fec/tests/types/bad_void.fe index bf3ce1f..4f983f2 100644 --- a/fec/tests/types/bad_void.fe +++ b/fec/tests/types/bad_void.fe @@ -1,3 +1,4 @@ +// ERROR:9:initializer type mismatch unit bad_void; fn noop() { diff --git a/fec/tests/types/badarr.fe b/fec/tests/types/badarr.fe index 11007cb..0fc94b5 100644 --- a/fec/tests/types/badarr.fe +++ b/fec/tests/types/badarr.fe @@ -1,3 +1,4 @@ +// ERROR:4:array element type mismatch unit badarr; fn main() -> i32 { let a: [2]i32 = [1, true, 3]; diff --git a/fec/tests/types/badchar.fe b/fec/tests/types/badchar.fe index 9d1a1d4..dbe2c10 100644 --- a/fec/tests/types/badchar.fe +++ b/fec/tests/types/badchar.fe @@ -1,3 +1,4 @@ +// ERROR:5:initializer type mismatch unit badchar; fn main() -> i32 { diff --git a/fec/tests/types/badcycle.fe b/fec/tests/types/badcycle.fe index 532ea1e..94fbd43 100644 --- a/fec/tests/types/badcycle.fe +++ b/fec/tests/types/badcycle.fe @@ -1,3 +1,4 @@ +// ERROR:2:by-value recursive type unit badcycle; struct A { b: B, } diff --git a/fec/tests/types/badfield.fe b/fec/tests/types/badfield.fe index 4fa664c..bfda363 100644 --- a/fec/tests/types/badfield.fe +++ b/fec/tests/types/badfield.fe @@ -1,3 +1,4 @@ +// ERROR:7:cannot assign through immutable value unit badfield; struct Point { x: i32, y: i32, } diff --git a/fec/tests/types/badfld.fe b/fec/tests/types/badfld.fe index 68704be..8ef5366 100644 --- a/fec/tests/types/badfld.fe +++ b/fec/tests/types/badfld.fe @@ -1,3 +1,4 @@ +// ERROR:5:missing struct field unit badfld; struct Point { x: i32, y: i32, } fn main() -> i32 { diff --git a/fec/tests/types/badindex.fe b/fec/tests/types/badindex.fe index 06b1b10..9db50a9 100644 --- a/fec/tests/types/badindex.fe +++ b/fec/tests/types/badindex.fe @@ -1,3 +1,4 @@ +// ERROR:6:cannot assign through immutable value unit badindex; fn main() -> i32 { diff --git a/fec/tests/types/badmat.fe b/fec/tests/types/badmat.fe index 3c7df6a..cc235a7 100644 --- a/fec/tests/types/badmat.fe +++ b/fec/tests/types/badmat.fe @@ -1,3 +1,4 @@ +// ERROR:5:non-exhaustive match unit badmat; enum Shape { Empty, Circle(i32), } fn main() -> i32 { diff --git a/fec/tests/types/badstr.fe b/fec/tests/types/badstr.fe index 6219d9a..8bcee1f 100644 --- a/fec/tests/types/badstr.fe +++ b/fec/tests/types/badstr.fe @@ -1,3 +1,4 @@ +// ERROR:5:cannot write through shared slice unit badstr; fn main() -> i32 { var text: str = "abc"; From de02b8bbbe51e4203438f87081f52d29b2422490 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 04:52:48 +0900 Subject: [PATCH 128/184] =?UTF-8?q?tests:=20bad=5Fbufw=20=EA=B0=80=20?= =?UTF-8?q?=EB=AC=B4=EC=97=87=EC=9D=84=20=EB=A7=89=EB=8A=94=EC=A7=80=20?= =?UTF-8?q?=EA=B8=B0=EB=A1=9D=ED=95=98=EA=B3=A0=20=EC=A7=84=EB=8B=A8?= =?UTF-8?q?=EC=9D=84=20=EA=B3=A0=EC=A0=95=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보고서가 '애매'로 남긴 하나다. io.buf_writer 는 351e5db 가 Writer 핸들 enum 으로 교체하면서 없앤 콜백 방식 writer이고, 이 fixture 는 그것이 돌아오지 않게 막으려고 그 커밋에서 추가됐다. 파일만 봐서는 알 수 없어 주석으로 남긴다. pin 94 -> 95. 마커 없는 fixture는 이제 없다. --- fec/tests/format/bad_bufw.fe | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fec/tests/format/bad_bufw.fe b/fec/tests/format/bad_bufw.fe index 37bf521..2ee7c87 100644 --- a/fec/tests/format/bad_bufw.fe +++ b/fec/tests/format/bad_bufw.fe @@ -1,3 +1,6 @@ +// ERROR:9:unknown name +// io.buf_writer was the callback-based writer that 351e5db replaced with the +// Writer handle enum. This fixture keeps it from coming back. unit bad_bufw; fn main() -> void { From 96e05aba5b1ae849e3f6f4523c0d84226b129030 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:00:31 +0900 Subject: [PATCH 129/184] =?UTF-8?q?refactor:=20=EB=B9=8C=EB=93=9C=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=EB=A5=BC=20=ED=95=98=EB=82=98=EC=9D=98=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=EA=B8=B0=EB=A1=9C=20=EB=B3=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 유닛마다 FeCheck 를 새로 만들면 타입 문맥도 유닛마다 따로 생겨서 유닛 경계를 넘는 이름을 볼 수가 없었다. 검사기가 빌드 전체를 맡고, 모든 유닛의 선언을 등록한 뒤에 어느 본문이든 보기 시작한다. 스코프와 심볼과 타입은 현재 유닛보다 오래 살아야 하므로 AST 아레나가 아니라 검사기 자신의 아레나에서 잡는다. 이름이 같아도 유닛이 다르면 다른 타입이므로 nominal 타입은 선언한 유닛으로도 구분한다. pub 은 파서가 버리고 있었다. 이제 FE_NODE_PUB 으로 남긴다. --- fec/src/ast.h | 6 +++ fec/src/check.c | 131 +++++++++++++++++++++++++++++++---------------- fec/src/check.h | 19 +++++-- fec/src/driver.c | 8 ++- fec/src/parser.c | 16 +++--- fec/src/types.c | 36 +++++++++++-- fec/src/types.h | 8 +++ 7 files changed, 161 insertions(+), 63 deletions(-) diff --git a/fec/src/ast.h b/fec/src/ast.h index efbf887..2118872 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -37,6 +37,12 @@ struct FeNode { unsigned flags; }; +/* Bits in FeNode.flags. 0x100 and above belong to own.h. */ +#define FE_NODE_PACKED 0x1U +#define FE_NODE_STATIC 0x2U +#define FE_NODE_SHARED 0x4U +#define FE_NODE_PUB 0x8U + typedef struct FeAst { FeArena arena; FeNode *root; diff --git a/fec/src/check.c b/fec/src/check.c index 3983a75..e900f4b 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -8,7 +8,7 @@ #include typedef struct FeSym FeSym; -typedef struct FeScope FeScope; +/* FeScope is forward declared in check.h. */ struct FeSym { const char *name; @@ -156,7 +156,7 @@ static char *unit_cname(FeCheck *c, const char *name) u = c->ast->root && c->ast->root->text ? c->ast->root->text : "unit"; n = (unsigned long)strlen("fe_") + (unsigned long)strlen(u) + (unsigned long)strlen(name ? name : "name") + 2UL; - p = (char *)fe_arena_alloc(&c->ast->arena, n); + p = (char *)fe_arena_alloc(&c->arena, n); if (!p) return 0; strcpy(p, "fe_"); strcat(p, u); @@ -173,7 +173,7 @@ static char *local_cname(FeCheck *c, const char *name) sprintf(number, "%u", c->local_serial++); n = (unsigned long)strlen("fe_l_") + (unsigned long)strlen(name) + (unsigned long)strlen(number) + 2UL; - p = (char *)fe_arena_alloc(&c->ast->arena, n); + p = (char *)fe_arena_alloc(&c->arena, n); if (!p) return 0; strcpy(p, "fe_l_"); strcat(p, name ? name : "local"); @@ -185,7 +185,7 @@ static char *local_cname(FeCheck *c, const char *name) static FeScope *scope_new(FeCheckerState *s, FeScope *parent) { FeScope *scope; - scope = (FeScope *)fe_arena_alloc(&s->c->ast->arena, sizeof(FeScope)); + scope = (FeScope *)fe_arena_alloc(&s->c->arena, sizeof(FeScope)); if (!scope) { err(s->c, s->c->ast->root->loc, "out of memory creating scope"); return parent; @@ -234,7 +234,7 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, } if (scope->count == scope->capacity) { capacity = scope->capacity ? scope->capacity * 2U : 8U; - items = (FeSym *)fe_arena_alloc(&s->c->ast->arena, + items = (FeSym *)fe_arena_alloc(&s->c->arena, capacity * sizeof(FeSym)); if (!items) { err(s->c, decl ? decl->loc : s->c->ast->root->loc, @@ -267,16 +267,38 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, return sym; } -void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, +/* Make `unit` the one being checked. Types intern against its name, cnames + are built from it, and diagnostics quote its source rather than whichever + file happened to be parsed last. */ +static void enter_unit(FeCheck *c, unsigned index) +{ + FeUnit *u = &c->build->units[index]; + c->unit = u; + c->ast = &u->ast; + c->types.unit_name = u->name[0] ? u->name : "unit"; + fe_diags_source(c->diags, u->source, u->size); +} + +void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, unsigned pointer_bits, int no_checks) { - c->ast = ast; + unsigned i; + fe_arena_init(&c->arena, 16384); + c->build = build; + c->unit = 0; + c->ast = build->count ? &build->units[0].ast : 0; + for (i = 0; i < FE_BUILD_UNIT_MAX; ++i) c->unit_scope[i] = 0; c->diags = diags; c->pointer_bits = pointer_bits; c->local_serial = 0; c->no_checks = no_checks; - fe_types_init(&c->types, &ast->arena, pointer_bits); - c->types.unit_name = ast->root && ast->root->text ? ast->root->text : "unit"; + fe_types_init(&c->types, &c->arena, pointer_bits); + c->types.unit_name = "unit"; +} + +void fe_check_destroy(FeCheck *c) +{ + fe_arena_destroy(&c->arena); } static FeType *check_expr(FeCheckerState *s, FeNode *n); @@ -517,7 +539,7 @@ static void own_release_after_stmt(FeCheckerState *s, FeScope *scope, static FeOwnState *flow_own_new(FeCheckerState *s, unsigned count) { if (!s || !count) return 0; - return (FeOwnState *)fe_arena_alloc(&s->c->ast->arena, + return (FeOwnState *)fe_arena_alloc(&s->c->arena, count*sizeof(FeOwnState)); } @@ -554,7 +576,7 @@ typedef struct FeFlowBorrow { static FeFlowBorrow *flow_borrow_new(FeCheckerState *s, unsigned count) { if (!s || !count) return 0; - return (FeFlowBorrow *)fe_arena_alloc(&s->c->ast->arena, + return (FeFlowBorrow *)fe_arena_alloc(&s->c->arena, count*sizeof(FeFlowBorrow)); } @@ -1737,7 +1759,7 @@ static void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) s.loop_depth=0; s.defer_depth=0; s.fn_node=fn; - fe_own_liveness_init(&s.liveness,&c->ast->arena); + fe_own_liveness_init(&s.liveness,&c->arena); fe_own_collect_last_uses(&s.liveness,fn); fn->sem_type = s.ret; for (x = fn->a ? fn->a->children : 0; x; x = x->next) { @@ -1765,7 +1787,7 @@ static void check_method(FeCheck *c, FeNode *fn, FeScope *globals, s.loop_depth=0; s.defer_depth=0; s.fn_node=fn; - fe_own_liveness_init(&s.liveness,&c->ast->arena); + fe_own_liveness_init(&s.liveness,&c->arena); fe_own_collect_last_uses(&s.liveness,fn); fn->sem_type=s.ret; for(x=fn->a ? fn->a->children : 0; x; x=x->next) { @@ -2776,26 +2798,13 @@ static void m7_validate_error_decl(FeCheck *c, FeNode *decl) } } -int fe_check_program(FeCheck *c) +/* Everything a unit declares, before any body anywhere is looked at. */ +static void declare_unit(FeCheck *c) { - FeCheckerState s; FeNode *n; - FeNode *m; - FeSym *sym; - FeType *t; - FeType *iv; - char method_name[128]; - s.c=c; - s.scope=scope_new(&s,0); - s.globals=s.scope; - s.ret=fe_type_intern(&c->types,"void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=0; - fe_own_liveness_init(&s.liveness,&c->ast->arena); for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_STRUCT) - fe_type_declare_struct(&c->types,n,(n->flags & 1U)!=0); + fe_type_declare_struct(&c->types,n,(n->flags & FE_NODE_PACKED)!=0); for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { m7_check_storage(c,n); if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); @@ -2805,7 +2814,20 @@ int fe_check_program(FeCheck *c) for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); check_type_cycles(c); - fe_type_layout_all(&c->types); +} + +/* The unit's top-level names, in a scope of their own so that another unit + can look into it later without inheriting anything else. */ +static FeScope *declare_unit_scope(FeCheck *c, FeCheckerState *s) +{ + FeNode *n; + FeNode *m; + FeType *t; + FeScope *globals; + char method_name[128]; + globals=scope_new(s,0); + s->scope=globals; + s->globals=globals; for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { if (n->kind==FE_N_STRUCT) { for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) { @@ -2816,21 +2838,31 @@ int fe_check_program(FeCheck *c) } if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { t=n->a ? node_type(c,n->a) : unknown(c); - add_symbol(&s,s.globals,n->text,t,0,n->kind==FE_N_GLOBAL, + add_symbol(s,globals,n->text,t,0,n->kind==FE_N_GLOBAL, n->b!=0,unit_cname(c,n->text ? n->text : "global"),n); } } for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_FN) { t=fe_type_intern(&c->types,""); - add_symbol(&s,s.globals,n->text,t,n,0,1, + add_symbol(s,globals,n->text,t,n,0,1, unit_cname(c,n->text ? n->text : "fn"),n); } + return globals; +} + +static void check_unit_bodies(FeCheck *c, FeCheckerState *s) +{ + FeNode *n; + FeNode *m; + FeSym *sym; + FeType *t; + FeType *iv; for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { - sym=find_current(s.globals,n->text ? n->text : ""); + sym=find_current(s->globals,n->text ? n->text : ""); if (n->b) { - iv=m7_check_expected(&s,n->b,sym ? sym->type : 0); + iv=m7_check_expected(s,n->b,sym ? sym->type : 0); if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { sym->type=iv; n->sem_type=iv; @@ -2840,27 +2872,40 @@ int fe_check_program(FeCheck *c) } } for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) check_fn(c,n,s.globals); + if (n->kind==FE_N_FN) check_fn(c,n,s->globals); for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_STRUCT) { t=fe_type_intern(&c->types,n->text); for (m=n->children;m;m=m->next) - if (m->kind==FE_N_FN) check_method(c,m,s.globals,t); + if (m->kind==FE_N_FN) check_method(c,m,s->globals,t); } - fe_type_layout_all(&c->types); - return c->diags->errors==0; } -FeType *fe_check_expr_type(FeCheck *c, FeNode *n) +int fe_check_program(FeCheck *c) { FeCheckerState s; + unsigned u; s.c=c; - s.scope=scope_new(&s,0); - s.globals=s.scope; + s.scope=0; + s.globals=0; s.ret=fe_type_intern(&c->types,"void"); s.loop_depth=0; s.defer_depth=0; s.fn_node=0; - fe_own_liveness_init(&s.liveness,&c->ast->arena); - return check_expr(&s,n); + fe_own_liveness_init(&s.liveness,&c->arena); + for (u=0;ubuild->count;++u) { enter_unit(c,u); declare_unit(c); } + fe_type_layout_all(&c->types); + for (u=0;ubuild->count;++u) { + enter_unit(c,u); + c->unit_scope[u]=declare_unit_scope(c,&s); + } + for (u=0;ubuild->count;++u) { + enter_unit(c,u); + s.scope=c->unit_scope[u]; + s.globals=c->unit_scope[u]; + check_unit_bodies(c,&s); + } + fe_type_layout_all(&c->types); + return c->diags->errors==0; } + diff --git a/fec/src/check.h b/fec/src/check.h index b26388a..dcb4da9 100644 --- a/fec/src/check.h +++ b/fec/src/check.h @@ -3,9 +3,22 @@ #include "types.h" #include "diag.h" +#include "resolve.h" +typedef struct FeScope FeScope; + +/* The checker spans a whole build, not one file. Names cross unit boundaries, + so every unit's declarations have to exist before any unit's bodies are + looked at, and they all have to be interned in one type context or the same + spelling in two units would not be the same type. */ typedef struct FeCheck { - FeAst *ast; + /* Scopes, symbols and types outlive whichever unit is current, so they + come from the checker's own arena rather than from an AST's. */ + FeArena arena; + FeBuild *build; + FeAst *ast; /* the unit being checked now */ + FeUnit *unit; /* its entry in the build */ + FeScope *unit_scope[FE_BUILD_UNIT_MAX]; FeTypeCtx types; FeDiags *diags; unsigned pointer_bits; @@ -13,9 +26,9 @@ typedef struct FeCheck { int no_checks; } FeCheck; -void fe_check_init(FeCheck *c, FeAst *ast, FeDiags *diags, +void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, unsigned pointer_bits, int no_checks); +void fe_check_destroy(FeCheck *c); int fe_check_program(FeCheck *c); -FeType *fe_check_expr_type(FeCheck *c, FeNode *n); #endif diff --git a/fec/src/driver.c b/fec/src/driver.c index 168380b..dddcd72 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -90,13 +90,11 @@ int main(int argc, char **argv) every unit's AST. */ { FeBuild build; - unsigned u; int ok=fe_build_load(&build,file,&d); - for(u=0;ok && usource,unit->size); - fe_check_init(&check,&unit->ast,&d,pointer_bits,no_checks); + if(ok){ + fe_check_init(&check,&build,&d,pointer_bits,no_checks); if(!fe_check_program(&check)) ok=0; + fe_check_destroy(&check); } fe_build_destroy(&build); /* Semantic analysis is the last pass there is. A code generator diff --git a/fec/src/parser.c b/fec/src/parser.c index 7d8a8e6..4fcc251 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -194,14 +194,14 @@ static FeNode *params(FeParser *p) static FeNode *fn_decl(FeParser *p, int pub, int external, int interrupt, int interrupt_safe) { FeToken t=p->current, name; FeNode *n; - (void)pub; (void)external; (void)interrupt; (void)interrupt_safe; + (void)external; (void)interrupt; (void)interrupt_safe; want(p,FE_TOK_FN,"expected 'fn'"); if(!is_name(p)){error(p,"expected function name");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"fn",2);} - name=p->current; n=toknode(p,FE_N_FN,t); n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n; + name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n; } -static FeNode *field(FeParser *p) +static FeNode *field(FeParser *p, int pub) { FeToken t=p->current; FeNode *n; - if(!is_name(p)){error(p,"expected field name");recover(p);return 0;} next(p);n=toknode(p,FE_N_FIELD,t);want(p,FE_TOK_COLON,"expected ':' after field");n->a=type(p);if(!eat(p,FE_TOK_COMMA) && !is(p,FE_TOK_RBRACE)) error(p,"expected ',' after field");return n; + if(!is_name(p)){error(p,"expected field name");recover(p);return 0;} next(p);n=toknode(p,FE_N_FIELD,t);if(pub)n->flags|=FE_NODE_PUB;want(p,FE_TOK_COLON,"expected ':' after field");n->a=type(p);if(!eat(p,FE_TOK_COMMA) && !is(p,FE_TOK_RBRACE)) error(p,"expected ',' after field");return n; } static FeNode *decl(FeParser *p) { @@ -214,11 +214,11 @@ static FeNode *decl(FeParser *p) if(!is(p,FE_TOK_PACKED)) t=p->current; if(is(p,FE_TOK_FN)) return fn_decl(p,pub,external,interrupt,interrupt_safe); if(eat(p,FE_TOK_PACKED)) t=p->previous; - if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(t.kind==FE_TOK_PACKED)n->flags|=1U;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){if(is(p,FE_TOK_PUB))next(p);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,0,0,0,0));else fe_node_add(n,field(p));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } - if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } - if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } + if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } + if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } + if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } - if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(kk==FE_TOK_STATIC)n->flags|=2U;if(shared)n->flags|=4U;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } + if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } error(p,"expected declaration"); before=p->current.kind; recover(p); if (p->current.kind==before && p->current.kind!=FE_TOK_EOF) next(p); return 0; diff --git a/fec/src/types.c b/fec/src/types.c index af2ff15..e3390cd 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -14,6 +14,7 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->name[i] = name[i]; t->name[i] = '\0'; t->kind = kind; + t->unit = 0; t->cname = 0; t->maker = 0; t->none_cname = 0; @@ -56,6 +57,28 @@ void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits) ctx->generated_serial = 0; } +/* Does this type answer to `name` for someone checking `unit`? A type with no + unit is shared by everyone; one with a unit answers only inside it. */ +static int type_visible_as(const FeType *t, const char *unit, const char *name) +{ + if (strcmp(t->name, name) != 0) return 0; + if (!t->unit) return 1; + return unit && strcmp(t->unit, unit) == 0; +} + +FeType *fe_type_intern_unit(FeTypeCtx *ctx, const char *unit, const char *name) +{ + FeType *t; + if (!name) name = ""; + if (!unit) return fe_type_intern(ctx, name); + for (t = ctx->types; t; t = t->next) + if (t->unit && strcmp(t->name, name) == 0 && + strcmp(t->unit, unit) == 0) return t; + t = new_type(ctx, name, FE_TYPE_UNKNOWN); + if (t) t->unit = unit; + return t; +} + FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) { FeType *t; @@ -64,7 +87,7 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) FeTypeKind kind = FE_TYPE_UNKNOWN; if (!name) name = ""; for (t = ctx->types; t; t = t->next) - if (strcmp(t->name, name) == 0) return t; + if (type_visible_as(t, ctx->unit_name, name)) return t; if (strcmp(name, "void") == 0) kind = FE_TYPE_VOID; else if (strcmp(name, "bool") == 0) kind = FE_TYPE_BOOL; else if (strcmp(name, "char") == 0) kind = FE_TYPE_CHAR; @@ -246,7 +269,7 @@ FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) unsigned i = 0; char *cname; if (!node || !node->text) return 0; - t = fe_type_intern(ctx, node->text); + t = fe_type_intern_unit(ctx, ctx->unit_name, node->text); if (t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_STRUCT) return t; if (t->kind == FE_TYPE_STRUCT) return t; t->kind = FE_TYPE_STRUCT; @@ -291,7 +314,7 @@ FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node) unsigned i = 0; char *cname; if (!node || !node->text) return 0; - t = fe_type_intern(ctx, node->text); + t = fe_type_intern_unit(ctx, ctx->unit_name, node->text); if (t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ENUM) return t; if (t->kind == FE_TYPE_ENUM) return t; t->kind = FE_TYPE_ENUM; @@ -568,7 +591,12 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) int fe_type_equal(const FeType *a, const FeType *b) { - return a == b || (a && b && strcmp(a->name, b->name) == 0); + if (a == b) return 1; + if (!a || !b) return 0; + if (strcmp(a->name, b->name) != 0) return 0; + /* The same spelling is not the same type across a unit boundary. */ + if (!a->unit || !b->unit) return a->unit == b->unit; + return strcmp(a->unit, b->unit) == 0; } int fe_type_is_integer(const FeType *t) diff --git a/fec/src/types.h b/fec/src/types.h index c5ddd88..c49d706 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -32,6 +32,10 @@ struct FeVariantType { struct FeType { FeTypeKind kind; char name[64]; + /* The unit that declared this type, for the nominal kinds. NULL for + builtins and for structural types like `[]u8`, which every unit + shares. Two units declaring the same name declare two types. */ + const char *unit; char *cname; char *maker; char *none_cname; @@ -76,6 +80,10 @@ typedef struct FeTypeCtx { void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits); FeType *fe_type_intern(FeTypeCtx *ctx, const char *name); +/* Intern a nominal type belonging to `unit` rather than to whichever unit + is being checked. Used to name a type across a unit boundary. */ +FeType *fe_type_intern_unit(FeTypeCtx *ctx, const char *unit, + const char *name); FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node); FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem); FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem); From a265901d7e02756286b505a5f871789f934daaa1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:06:16 +0900 Subject: [PATCH 130/184] =?UTF-8?q?implement:=20=EC=9C=A0=EB=8B=9B=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=EB=A5=BC=20=EB=84=98=EB=8A=94=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84,=20=EA=B0=80=EC=8B=9C=EC=84=B1,=20=EA=B8=B0=EB=B3=B8?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=A7=91=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit import 가 만든 binding 으로 다른 유닛의 선언에 닿는다. 호출, 구조체 리터럴, 값 참조 세 자리다. 시그니처의 타입은 그 시그니처가 쓰인 유닛에서 해석한다 -- 호출한 쪽에서 해석하면 같은 이름이 다른 타입을 가리킨다. pub 없는 선언과 필드는 자기 유닛 밖에서 보이지 않는다. error.Name 은 구현된 적이 없었다. 기본 에러 집합 core.Error 의 멤버이고, 그 집합은 선언이 아니라 수집으로 채워지므로 변이 목록 없이 정체성만 갖는다. units 34/34. dotpriv/main 은 위반이 있는 유닛을 import 하므로 받아들여질 수 없다 -- 마커를 붙이고 이유를 적었다. --- fec/src/check.c | 280 ++++++++++++++++++++++++++------ fec/src/types.c | 6 + fec/tests/units/dotpriv/main.fe | 4 + 3 files changed, 243 insertions(+), 47 deletions(-) diff --git a/fec/src/check.c b/fec/src/check.c index e900f4b..8d0d3e7 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -301,6 +301,74 @@ void fe_check_destroy(FeCheck *c) fe_arena_destroy(&c->arena); } +static unsigned unit_index(FeCheck *c, const FeUnit *u) +{ + return (unsigned)(u - c->build->units); +} + +/* An import introduces a local binding, so `binding.name` reaches into the + unit it names. A local of the same spelling wins -- shadowing a binding is + legal and means the local -- so this only answers when the base name is not + otherwise in scope. */ +static FeUnit *binding_unit(FeCheckerState *s, FeNode *base) +{ + if (!base || base->kind!=FE_N_IDENT || !base->text) return 0; + if (!s->c->build || !s->c->unit) return 0; + if (find_symbol(s->scope,base->text)) return 0; + return fe_build_binding(s->c->build,s->c->unit,base->text); +} + +/* SPEC 8.2: a declaration is visible outside its unit only with `pub`. */ +static int decl_is_public(const FeNode *decl) +{ + return decl && (decl->flags & FE_NODE_PUB)!=0; +} + +static FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name) +{ + if (!u || !name) return 0; + return find_current(c->unit_scope[unit_index(c,u)],name); +} + +/* A type another unit declares, or null if it declares no such type. Interning + is keyed on the declaring unit, so this cannot collide with a same-named + type here. */ +static FeType *unit_type(FeCheck *c, FeUnit *u, const char *name) +{ + FeType *t; + if (!u || !name) return 0; + for (t=c->types.types;t;t=t->next) + if (t->unit && strcmp(t->name,name)==0 && + strcmp(t->unit,u->name)==0 && t->kind!=FE_TYPE_UNKNOWN) return t; + return 0; +} + +/* The AST declaration of a type another unit declares, for its visibility and + for its methods. */ +static FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name) +{ + FeNode *n; + (void)c; + if (!u || !name) return 0; + for (n=u->ast.root ? u->ast.root->children : 0;n;n=n->next) + if ((n->kind==FE_N_STRUCT || n->kind==FE_N_ENUM || + n->kind==FE_N_ERROR_DECL) && n->text && + strcmp(n->text,name)==0) return n; + return 0; +} + +/* Resolve a type written in another unit's source. Names in a signature mean + what they meant where the signature was written, not where it is called. */ +static FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node) +{ + const char *save=c->types.unit_name; + FeType *t; + if (unit) c->types.unit_name=unit; + t=node_type(c,node); + c->types.unit_name=save; + return t; +} + static FeType *check_expr(FeCheckerState *s, FeNode *n); static FeNode *find_method(FeCheck *c, FeType *owner, const char *name) @@ -335,6 +403,9 @@ static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, FeType *base_in); static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read); static FeType *check_call(FeCheckerState *s, FeNode *n); +static FeType *check_call_fn(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home); +static int is_error_set_member(FeCheckerState *s, FeNode *n); typedef struct FeFlowSlot { FeSym *sym; @@ -770,6 +841,42 @@ static int has_field(FeNode *list, const char *name) return 0; } +/* A field of a type declared elsewhere is reachable only with `pub`. Inside + the declaring unit every field is reachable, `pub` or not. */ +static int field_is_visible(FeCheckerState *s, const FeType *t, + const FeFieldType *field) +{ + if (!t || !t->unit) return 1; + if (s->c->types.unit_name && + strcmp(t->unit,s->c->types.unit_name)==0) return 1; + return field && field->ast_node && + (field->ast_node->flags & FE_NODE_PUB)!=0; +} + +/* The field list of a struct literal, once the type is known. Reached from + both `Type{...}` and `binding.Type{...}`. */ +static FeType *check_struct_fields(FeCheckerState *s, FeNode *n, FeType *t) +{ + FeFieldType *field; + FeNode *f; + FeType *v; + unsigned i; + for(f=n->children;f;f=f->next) if(f->kind==FE_N_FIELD) { + if(has_field(f->next,f->text)) { err(s->c,f->loc,"duplicate struct field"); } + field=fe_type_field(t,f->text); + if(!field) { err(s->c,f->loc,"invalid struct field"); continue; } + if(!field_is_visible(s,t,field)) { + err(s->c,f->loc,"field is private to its unit"); + continue; + } + v=check_expr(s,f->a); + mark_moved(s,f->a,v); + if(!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"struct field type mismatch"); + } + for(i=0;ifield_count;i++) if(!has_field(n->children,t->fields[i].name)) err(s->c,n->loc,"missing struct field"); + n->sem_type=t; return t; +} + static FeType *check_struct_init(FeCheckerState *s, FeNode *n) { FeType *t; @@ -778,8 +885,24 @@ static FeType *check_struct_init(FeCheckerState *s, FeNode *n) FeType *v; FeType *et; FeVariantType *variant; - unsigned i; if (n->a && n->a->kind == FE_N_MEMBER) { + FeUnit *home=binding_unit(s,n->a->a); + if (home) { + /* `binding.Type{...}` names a type in another unit. */ + const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; + FeNode *decl=unit_type_decl(s->c,home,want); + t=unit_type(s->c,home,want); + if (!t || !decl) { err(s->c,n->a->loc,"unknown name"); return unknown(s->c); } + if (!decl_is_public(decl)) { + err(s->c,n->a->loc,"type is private to its unit"); + return unknown(s->c); + } + if (t->kind!=FE_TYPE_STRUCT) { + err(s->c,n->loc,"unknown struct type"); + return unknown(s->c); + } + return check_struct_fields(s,n,t); + } et=check_expr(s,n->a->a); variant=et && et->kind==FE_TYPE_ENUM ? fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; @@ -802,16 +925,7 @@ static FeType *check_struct_init(FeCheckerState *s, FeNode *n) } t=fe_type_intern(&s->c->types,n->text ? n->text : ""); if (!t || t->kind!=FE_TYPE_STRUCT) { err(s->c,n->loc,"unknown struct type"); return unknown(s->c); } - for(f=n->children;f;f=f->next) if(f->kind==FE_N_FIELD) { - if(has_field(f->next,f->text)) { err(s->c,f->loc,"duplicate struct field"); } - field=fe_type_field(t,f->text); - if(!field) { err(s->c,f->loc,"invalid struct field"); continue; } - v=check_expr(s,f->a); - mark_moved(s,f->a,v); - if(!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"struct field type mismatch"); - } - for(i=0;ifield_count;i++) if(!has_field(n->children,t->fields[i].name)) err(s->c,n->loc,"missing struct field"); - n->sem_type=t; return t; + return check_struct_fields(s,n,t); } static FeType *check_array_init(FeCheckerState *s, FeNode *n) @@ -1059,6 +1173,22 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) if (n->a && n->a->kind == FE_N_MEMBER) { FeNode *method; FeNode *self_param; + FeUnit *home=binding_unit(s,n->a->a); + if (home) { + const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; + FeSym *fsym=unit_member(c,home,want); + if (!fsym) { + err(c,n->a->loc,"unknown name"); + for (x=n->children;x;x=x->next) check_expr(s,x); + return unknown(c); + } + if (!decl_is_public(fsym->decl)) { + err(c,n->a->loc,"name is private to its unit"); + for (x=n->children;x;x=x->next) check_expr(s,x); + return unknown(c); + } + return check_call_fn(s,n,fsym,home->name); + } et=check_expr(s,n->a->a); method=et && et->kind==FE_TYPE_STRUCT ? find_method(c,et,n->a->b ? n->a->b->text : "") : 0; @@ -1113,47 +1243,16 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) err(c, n->loc, "unknown function"); return unknown(c); } - n->a->cname = sym->cname; - n->sem_decl = sym->fn; - if (!sym->fn) { - err(c, n->loc, "name is not a function"); - return unknown(c); - } - param = sym->fn->a ? sym->fn->a->children : 0; - arg = n->children; - while (param && arg) { - a = check_expr(s, arg); - b = node_type(c, param->a); - if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && - a->kind==FE_TYPE_REF && a->ref_mut) { - FeSym *root=own_root_symbol(s,arg); - if (root && root->borrow_root) root=root->borrow_root; - if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); - } else if (b && a && b->kind==FE_TYPE_SLICE && !b->ref_mut && - a->kind==FE_TYPE_SLICE && a->ref_mut) { - /* Call-only []mut -> [] weakening is a temporary view. */ - } else mark_moved(s,arg,a); - if (!compatible(b, a, arg) && - !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - a->kind != FE_TYPE_UNKNOWN) - err(c, arg->loc, "argument type mismatch"); - own_release_temporary_borrow(s,arg); - param = param->next; - arg = arg->next; - } - if (param || arg) err(c, n->loc, "wrong number of arguments"); - a = sym->fn->b ? node_type(c, sym->fn->b) : - fe_type_intern(&c->types, "void"); - n->sem_type = a; - return a; + return check_call_fn(s, n, sym, 0); } for (x = n->children; x; x = x->next) check_expr(s, x); return unknown(c); } if (n->kind == FE_N_MEMBER) { + if (is_error_set_member(s,n)) { + n->sem_type=fe_type_intern(&c->types,"core.Error"); + return n->sem_type; + } if (n->a && n->a->kind==FE_N_IDENT && n->a->text && strcmp(n->a->text,"io")==0 && n->b && n->b->text && (strcmp(n->b->text,"stdout")==0 || @@ -1898,6 +1997,85 @@ static int m7_place_is_projection(FeNode *n) return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); } +/* A call to a named function. `home` is the unit the signature was written in, + null when that is the unit being checked: parameter and return types have to + be read where they were written or a name would mean the caller's type. */ +/* `error.Name` is a member of the default error set. That set is open -- names + are collected across the build and numbered later, not declared -- so any + name is well formed here and the value's type is core.Error. */ +static int is_error_set_member(FeCheckerState *s, FeNode *n) +{ + return n && n->kind==FE_N_MEMBER && n->a && n->a->kind==FE_N_IDENT && + n->a->text && strcmp(n->a->text,"error")==0 && + n->b && n->b->text && !find_symbol(s->scope,"error"); +} + +/* `binding.name` used as a value rather than called. */ +static FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled) +{ + FeUnit *home=binding_unit(s,n->a); + FeSym *sym; + *handled=0; + if (!home) return 0; + *handled=1; + sym=unit_member(s->c,home,n->b && n->b->text ? n->b->text : ""); + if (!sym) { err(s->c,n->loc,"unknown name"); return unknown(s->c); } + if (!decl_is_public(sym->decl)) { + err(s->c,n->loc,"name is private to its unit"); + return unknown(s->c); + } + n->cname=sym->cname; + n->sem_decl=sym->decl; + n->sem_type=sym->type; + return sym->type; +} + +static FeType *check_call_fn(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home) +{ + FeCheck *c=s->c; + FeNode *param; + FeNode *arg; + FeType *a; + FeType *b; + if (n->a) n->a->cname = sym->cname; + n->sem_decl = sym->fn; + if (!sym->fn) { + err(c, n->loc, "name is not a function"); + return unknown(c); + } + param = sym->fn->a ? sym->fn->a->children : 0; + arg = n->children; + while (param && arg) { + a = check_expr(s, arg); + b = node_type_in(c, home, param->a); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (b && a && b->kind==FE_TYPE_SLICE && !b->ref_mut && + a->kind==FE_TYPE_SLICE && a->ref_mut) { + /* Call-only []mut -> [] weakening is a temporary view. */ + } else mark_moved(s,arg,a); + if (!compatible(b, a, arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind != FE_TYPE_UNKNOWN) + err(c, arg->loc, "argument type mismatch"); + own_release_temporary_borrow(s,arg); + param = param->next; + arg = arg->next; + } + if (param || arg) err(c, n->loc, "wrong number of arguments"); + a = sym->fn->b ? node_type_in(c, home, sym->fn->b) : + fe_type_intern(&c->types, "void"); + n->sem_type = a; + return a; +} + static FeType *check_call(FeCheckerState *s, FeNode *n) { FeCheck *c; @@ -2166,6 +2344,14 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) if (n->kind==FE_N_CALL) return check_call(s,n); if (n->kind==FE_N_MEMBER) { + int handled; + FeType *cross; + if (is_error_set_member(s,n)) { + n->sem_type=fe_type_intern(&s->c->types,"core.Error"); + return n->sem_type; + } + cross=cross_unit_value(s,n,&handled); + if (handled) return cross; a=check_expr(s,n->a); return m7_member_field(s,n,a); } diff --git a/fec/src/types.c b/fec/src/types.c index e3390cd..290f756 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -96,6 +96,11 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) else if (strcmp(name, "io.Writer") == 0) { kind = FE_TYPE_STRUCT; } + /* The default error set. Its members are collected across the build rather + than declared, so it carries an identity but no variant list. */ + else if (strcmp(name, "core.Error") == 0) { + kind = FE_TYPE_ENUM; bits = 16; uns = 1; + } else if (strcmp(name, "i8") == 0 || strcmp(name, "u8") == 0) { kind = FE_TYPE_INT; bits = 8; uns = name[0] == 'u'; } else if (strcmp(name, "i16") == 0 || strcmp(name, "u16") == 0) { @@ -109,6 +114,7 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) if (!t) return 0; t->bits = bits; t->is_unsigned = uns; + if (strcmp(name,"core.Error")==0) { t->is_error = 1; t->size = 2; t->align = 2; } if (strcmp(name,"io.Writer")==0) { t->cname=fe_arena_strdup(ctx->arena,"fe_writer",10); t->size=4; diff --git a/fec/tests/units/dotpriv/main.fe b/fec/tests/units/dotpriv/main.fe index b14ac8d..3873e01 100644 --- a/fec/tests/units/dotpriv/main.fe +++ b/fec/tests/units/dotpriv/main.fe @@ -1,3 +1,7 @@ +// ERROR:private +// The violation is in game/bar.fe, which this unit imports. Reaching it from +// any entry has to be rejected, so the marker pins the message and not a line +// in another file. unit main; import game.bar; From cb20cce81a1a553338b434806af80f946cee8c54 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:08:07 +0900 Subject: [PATCH 131/184] =?UTF-8?q?docs:=20TODO=20=EC=99=80=20=ED=95=B8?= =?UTF-8?q?=EB=93=9C=EC=98=A4=ED=94=84=20=EB=AC=B8=EC=84=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EC=A0=81=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 지난번 에이전트가 handoff1 만 지우라는 지시에 두 문서까지 같이 지웠다. untracked 였기 때문에 git 에 기록이 없어 복구할 수 없었다. --- TODO.md | 115 +++++++++++++++++++++++++ handoff2.md | 244 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 359 insertions(+) create mode 100644 TODO.md create mode 100644 handoff2.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..828d202 --- /dev/null +++ b/TODO.md @@ -0,0 +1,115 @@ +# TODO + +현재: **150/188** (`uv run python tests/run.py`) + +컴파일러는 프론트엔드뿐이다. IR·lowering·백엔드는 없다. + +--- + +## 프론트엔드 마무리 + +| # | 일 | 규모 | fixture | 비고 | +|---|---|---|---|---| +| 1 | cross-unit 이름 해석 | 중 | units 8 | `util.answer()` 가 다른 유닛 선언을 보게. `check.c` 가 import binding 을 심볼로 알아야 함 | +| 2 | 가시성 `pub`/private | 중 | units 4 | `privfn` `privfld` `dotpriv` `pubpriv` | +| 3 | nominal error identity | 중 | units 3 | `errsame` `errdet` `errnom` | +| 4 | **제네릭 실체화** | 대 | generic 25 | worklist 엔진. **IR 설계에 직접 영향 — 분기점** | +| 5 | 마커 없는 fixture 37개 판정 | 중 | — | 개당 판단 필요. 아래 참조 | + +## 백엔드 + +| # | 일 | 규모 | 비고 | +|---|---|---|---| +| 6 | IR 정의 | 중 | 초안 있음. 4번이 정해져야 확정 | +| 7 | **lowering** | 대 | `try`/`catch`/`defer`/drop/`for`/메서드/경계검사 전개. **프로젝트 무게중심** | +| 8 | i386 백엔드 | 대 | IR → x86 asm → `wasm` → `wlink`. W11 에서 실행 검증 | +| 9 | 런타임 최소 세트 | 소~중 | 시작 스텁, `fe_trap`, `malloc`/`free` | +| 10 | `pending-backend/` 3개 복귀 | 소 | bounds trap, `--no-checks` 차등, 소유권 drop | + +## 그 이후 + +| # | 일 | 규모 | 비고 | +|---|---|---|---| +| 11 | stdlib | 대 | 현재 49줄, 본문 있는 함수 2개. `List`/`Map` 은 4번에 의존 | +| 12 | bits16 백엔드 | 대 | `DX:AX` 32비트 산술, 세그먼트. 제일 어려움. 마지막 | +| 13 | 타깃 의존 구문 | 중 | `far` `asm` `interrupt` `atomic` `critical` `shared` — 파싱만 되고 의미 없음. fixture 도 없음 | + +--- + +## 미결 결정 + +| | 내용 | 언제 | +|---|---|---| +| 덩어리 전달 규약 | 정함 (전부 주소로). 8번 착수 전 재확인 | 8 전 | +| `extern` C 상호운용 | 위 규약이 C ABI 와 안 맞음. 경계 변환 필요 | 8 전 | +| 주 타깃 | i386/W11 로 기울었으나 SPEC §1.2 는 아직 "640KB 셀프호스팅" | 아무 때나 | +| stdlib 명세 | SPEC §10 이 플레이스홀더. 시그니처·오류·경계 동작 미정 | 11 전 | +| `far` 의 거취 | IR 에 안 두기로 함. 언어에서 뺄지 bits16 전용으로 둘지는 미정 | 12 전 | + +--- + +## 순서 + +``` +1 → 2 → 3 프론트엔드 유닛 완성 +4 제네릭. ← 여기서 IR 형태가 결정됨 +6 → 7 IR + lowering +8 → 9 → 10 i386 백엔드, 실행 검증 복귀 +11 stdlib +12 → 13 DOS 고유 +``` + +5번은 독립적이라 아무 때나 끼워 넣는다. + +--- + +## 위임 정책 + +**위임 가능** — 결과가 자명하고 판단이 없는 것 + +- fixture 이름 개선, README 갱신, 문서 정합 +- 파일 목록을 **명시적으로 열거해서** 줄 것. "찾아서 하라" 는 약함 +- 기준선 두 숫자(`N/188`, `M pin`)를 주고 **변하면 안 된다**고 못박을 것 + +**위임 불가** — 언어 의미론 판단이 섞인 것 + +- 마커 판정, cross-unit 해석, 가시성, 제네릭, IR/lowering/백엔드 + +--- + +## 5번에 대하여 (마커 없는 fixture 37개) + +거부 fixture 76개 중 **37개에 `// ERROR:` 마커가 없다.** 마커가 없으면 러너는 +"거부되기만 하면 통과" 로 판정한다. 즉 **엉뚱한 이유로 거부돼도 초록이다.** + +``` +format 10 own 8 types 19 +``` + +마커를 붙이려면 fixture 마다 "지금 나오는 진단이 옳은가" 를 판정해야 하고, +그 판정은 세 갈래로 갈린다. 오늘 5건을 조사했을 때 실제로 셋 다 나왔다: + +| 결론 | 예 | 조치 | +|---|---|---| +| 마커가 틀렸다 | `badbrmov` 한 줄 밀림 | 마커 수정 | +| 마커가 언어를 오해했다 | `badloop` — 루프 안에서 이미 걸림 | 마커 수정 | +| **진단이 부실하다** | `badweak` — "type mismatch" 로만 나옴 | **컴파일러 수정** | + +세 번째 때문에 위임이 위험하다. 실제 출력을 마커에 그대로 베끼면 전부 초록이 +되지만 **컴파일러가 틀린 곳까지 정답으로 굳는다.** + +### 병렬화 + +판정 자체는 **fixture 간 독립**이라 병렬 가능하다. 다만 **판정을 위임하면 안 된다.** +쓸 수 있는 형태는 증거 수집과 판정을 나누는 것이다: + +``` +위임: fixture 를 열고 → fec 를 돌리고 → 다음을 보고 + 파일 경로 / 검사하려는 것으로 보이는 규칙 / 실제 진단 전문(줄·문구) + 마커는 만들지 않는다 + +직접: 보고를 읽고 세 갈래 중 무엇인지 판정 → 마커를 쓰거나 컴파일러를 고침 +``` + +이러면 37개를 디렉터리별로 병렬로 뿌려 증거를 모으고, 판정만 직접 하면 된다. +느린 부분(파일 읽기·실행·정리)이 병렬화되고 위험한 부분은 남는다. diff --git a/handoff2.md b/handoff2.md new file mode 100644 index 0000000..d051e48 --- /dev/null +++ b/handoff2.md @@ -0,0 +1,244 @@ +# Handoff 2 — 마커 증거 수집과 fixture 이름 짓기 + +앞선 시도가 한 번 실패해서 되돌렸다(`7636a41`). 실패 원인부터 읽어라. +그게 이 문서의 절반이다. + +## 실패한 방식 + +지난번 에이전트는 **파일을 열지 않고 이름 문자열만 변환했다.** 밑줄을 지우고, +그 결과 충돌하는 이름에 숫자를 붙였다. + +``` +bad_loop → badlop1 badweak → badweak2 +badloop → badlp2 oktry → oktry2 +``` + +결정적 증거는 이것이다. `own/badfld.fe` 는 **구조체 필드에 참조를 둔 것**을 검사하고 +`types/badfld.fe` 는 **없는 필드에 접근한 것**을 검사한다. 완전히 다른 규칙인데 +둘 다 `badfmem.fe` 가 됐다. 같은 옛 이름에 같은 치환을 먹였기 때문이다. + +**이 일의 본질은 파일을 읽고 무엇을 검사하는지 판단하는 것이다.** 이름 변환이 아니다. + +--- + +## 배경 + +`fec` 는 Ferro 언어의 컴파일러이고 현재 **프론트엔드만** 있다. fixture 는 컴파일러가 +어떤 코드를 받아들이고 어떤 코드를 거부하는지 고정하는 테스트다. 파일 하나가 케이스 +하나다. + +```powershell +uv run python tests/run.py # 전체 +uv run python tests/run.py -k own # 경로에 own 이 들어간 것만 +``` + +러너는 매번 `.build\fec.exe` 를 새로 빌드한다. 진단 전문을 보려면 그걸 직접 부른다. + +``` +> .build\fec.exe --check fec\tests\types\bad_ari.fe +fec/tests/types/bad_ari.fe:8:15: error: wrong number of arguments + 8 | return add(1); + | ^ +``` + +### 러너가 기대를 정하는 방법 — 이걸 정확히 알아야 한다 + +`tests/run.py` 의 `expectation()` 을 직접 읽어라. 요약하면: + +| 파일 첫 줄 | 러너의 기대 | +|---|---| +| `// ERROR:7:borrow` | **거부**되고, 진단이 **7번 줄**, 문구에 **`borrow`** 포함 | +| `// ERROR:borrow` | 거부되고 문구에 `borrow` 포함 (줄은 안 봄) | +| **마커 없음** | **파일명이 `bad` 로 시작하면 거부**, 아니면 **성공** | + +마지막 줄이 이 작업의 핵심 함정이다. + +> ### 마커가 없는 파일에서는 `bad` 접두사가 기대값 그 자체다 +> +> 마커 없는 `bad_ari.fe` 를 `arity.fe` 로 바꾸면 러너는 그 순간부터 +> **성공하기를** 기대한다. 그리고 그 fixture 는 실패한다. +> +> 마커가 **있는** 파일은 마커가 기대를 정하므로 접두사가 아무 의미도 없다. +> 마음대로 지어도 된다. + +그래서 이 핸드오프는 마커 없는 37개를 **먼저** 처리한다. + +--- + +## 착수 전 기준선 + +``` +uv run python tests/run.py +→ 150/188 passed (58 pin a line and message) +``` + +**두 숫자 모두 끝까지 변하면 안 된다.** + +- 줄면 무언가 깨진 것이다 +- **늘면 검사를 약화시킨 것이다.** 이쪽이 더 나쁘다. 조용히 통과하는 테스트는 + 없는 테스트보다 해롭다 + +어느 쪽이든 되돌리고 보고하라. + +--- + +# 1. 마커 없는 fixture 37개 — 증거만 모은다 + +이 37개는 마커가 없어서 **"거부되기만 하면 통과"** 다. 엉뚱한 이유로 거부돼도 초록이다. +마커를 붙여야 하는데 **그 판정은 네가 하지 않는다.** + +``` +types/ 19 bad_ari bad_asgn bad_cast bad_cond bad_mlet bad_ret bad_shwr + bad_type bad_unit bad_unk bad_void badarr badchar badcycle + badfield badfld badindex badmat badstr + +format/ 10 bad_ari bad_bufw bad_cls bad_many bad_open bad_run bad_try + bad_type bad_verb bad_writ + +own/ 8 bad_clos bad_cond bad_dbl bad_dest bad_drop bad_loop bad_move + bad_proj +``` + +## 왜 판정을 맡기지 않는가 + +판정 결과가 세 갈래로 갈리는데 그중 하나는 **컴파일러를 고쳐야 하는 경우**다. + +| 결론 | 조치 | +|---|---| +| 마커를 안 붙였을 뿐, 진단은 옳다 | 마커를 쓴다 | +| 진단은 나오지만 **다른 이유**로 거부하고 있다 | fixture 를 다시 본다 | +| **진단이 부실하다** — 규칙 위반을 못 짚고 뭉뚱그린 오류만 낸다 | **컴파일러를 고친다** | + +세 번째가 실제로 있었다. `own/badweak.fe` 는 mut 대여를 shared 로 약화시키는 것을 +검사하는데 컴파일러는 `type mismatch` 라고만 했다. 실제 출력을 마커에 그대로 +베꼈다면 초록이 되면서 **컴파일러의 부실한 진단이 정답으로 굳었을 것이다.** + +그래서 **너는 증거를 모으고 판정은 사람이 한다.** + +## 파일마다 보고할 것 + +``` +파일 fec/tests/types/bad_ari.fe +검사 대상 이 코드가 무엇을 위반하려 하는가 — 네가 읽고 판단한 것 +근거 그렇게 본 이유. 어느 줄의 무엇 때문인지 +실제 진단 .build\fec.exe --check <경로> 의 출력 전문 (줄·열·문구 그대로) +일치 여부 실제 진단이 '검사 대상' 을 짚는가 — 예 / 아니오 / 애매 +``` + +`일치 여부` 가 이 작업의 산출물이다. 나머지는 그 판단의 근거다. +**애매하면 애매하다고 써라.** 억지로 '예' 로 만들면 이 작업이 무의미해진다. + +## 절대 금지 + +- **`// ERROR:` 마커를 하나도 쓰지 마라.** 이 항목의 산출물은 보고서뿐이다 +- `fec/src/` 의 어떤 파일도 고치지 마라 +- `tests/run.py` 를 고치지 마라 (읽는 건 권장) +- fixture 의 내용을 고치지 마라 +- **실제 출력을 그대로 마커로 옮기는 것** — 가장 하기 쉽고 가장 해로운 실수다 + +## 산출물 + +리포지터리 루트에 `fixture-report.md`. 디렉터리별로 나누고 위 다섯 항목을 담는다. +이것만 커밋한다. + +--- + +# 2. fixture 이름 짓기 + +1번을 **끝내고 보고한 뒤에** 시작한다. 1번의 판정 결과가 이름을 바꾸기 때문이다. + +대상은 네 디렉터리의 모든 `.fe` 파일이다. + +``` +fec/tests/types/ 31 +fec/tests/format/ 13 +fec/tests/own/ 50 +fec/tests/optional/ 28 +``` + +`units/` `generic/` `parse/` `pending-backend/` 는 **제외한다.** +(`units/` 와 `generic/` 은 이름이 import 경로의 일부라 구조가 다르다.) + +이미 제대로 된 이름 셋은 손대지 마라. + +``` +own/globalm.fe own/localesc.fe own/self_fld.fe +``` + +## 이름 제약 (컴파일러가 강제한다) + +각 `.fe` 는 `unit <이름>;` 을 갖고 **그 이름이 파일명(확장자 제외)과 정확히 같아야 한다.** +이름은 소문자로 시작, `a-z0-9_` 만, **최대 8자**. DOS 8.3 에서 온 제약이고 +`SPEC.md` §8.1 의 일부라 바꿀 수 없다. + +### 디렉터리가 다르면 이름이 겹쳐도 된다 + +지금 `types/bad_ari.fe` 와 `format/bad_ari.fe` 가 **동시에 존재하고 테스트는 통과한다.** +각 fixture 는 독립된 빌드다. 지난번 실패는 이걸 몰라서 억지로 유일하게 만들려다 +숫자를 붙인 것이다. **각 디렉터리 안에서만 유일하면 된다.** + +### 접두사 + +| | | +|---|---| +| **마커가 있는 파일** | `bad`/`ok` 접두사를 **버려도 된다.** 기대는 마커가 정한다. 8자를 접두사에 쓰지 마라 | +| **1번의 37개** | 사람이 마커를 붙이기 전까지 **`bad` 접두사를 반드시 유지해야 한다.** 남는 건 5자다 | + +37개는 5자 안에 뜻을 담기 어려우니 **가능한 만큼만 개선하고, 안 되는 건 그대로 두고 +목록에 적어라.** 마커가 붙으면 그때 다시 짓는다. + +## 절차 + +파일 하나마다: + +1. **연다.** 전체를 읽는다. 대개 10줄 미만이다 +2. **무엇을 검사하는지 판단한다.** 마커가 있으면 강한 단서다 +3. 그것을 8자 안에 나타내는 이름을 짓는다 +4. `git mv` 로 옮긴다 +5. **파일 안 `unit` 선언을 새 이름으로 고친다.** 안 하면 컴파일러가 거부한다 +6. 그 외에는 파일을 **한 글자도** 건드리지 마라 + +## 이름의 기준 + +이름은 **"무엇을 검사하는가"** 를 나타낸다. + +``` +좋음: badgmut → globalm 전역을 mut 로 빌리는 것 + badlocsl → localesc 지역 변수의 참조가 탈출하는 것 + badfld → reffield 구조체 필드에 참조를 둔 것 (own/) + badfld → nofield 없는 필드에 접근한 것 (types/) + +나쁨: badarr → ba1 아무것도 말하지 않음 + badweak → badweak2 숫자는 정보가 아님 + badcatch → badcatc 그냥 자른 것 +``` + +축약은 해도 된다. 다만 **읽어서 짐작이 가야 한다.** + +## 절대 금지 + +- **첫 줄 `// ERROR:` 마커를 만들거나 고치거나 지우지 마라** +- `unit` 선언 외의 내용 변경 +- 빈 줄 추가 — 지난번에 119개 파일에 군더더기 빈 줄이 들어갔다 + +## 진행 방법 + +**디렉터리 하나씩** 끝내고 `-k <디렉터리>` 로 확인한 뒤 커밋해라. 범위가 좁아야 +문제를 찾는다. + +--- + +# 커밋 + +1번은 하나, 2번은 디렉터리마다 하나. 메시지는 무엇을 왜 바꿨는지 한국어로. +**푸시하지 마라.** + +# 최종 보고 + +- 각 커밋 전후의 두 숫자 (`N/188`, `M pin`) +- 1번: `fixture-report.md` 경로, 그리고 **`일치 여부: 아니오 / 애매`** 로 판정한 것의 목록. + 거기가 컴파일러를 고쳐야 할 수도 있는 지점이라 가장 중요하다 +- 2번: **바꾼 이름 전체 목록** — `디렉터리/이전 → 이후` 와 각각 **한 줄 근거**. + 근거가 안 써지는 이름은 잘못 지은 것이다 +- 2번에서 이름을 못 지은 파일 목록과 이유. 억지로 짓지 말고 남겨라 +- 판단이 필요해서 건너뛴 것 From 25d9de65e1117b935d0c76617327a1d795debbaa Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:24:36 +0900 Subject: [PATCH 132/184] =?UTF-8?q?implement:=20=EC=A0=9C=EB=84=A4?= =?UTF-8?q?=EB=A6=AD=20=EB=AA=A8=EB=85=B8=EB=AA=A8=ED=94=BC=EC=A0=9C?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20(SPEC=209)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 타입 인자를 바인딩한 상태로 선언을 인스턴스마다 한 번씩 검사한다. 바인딩된 이름은 그냥 그 인자 타입이므로 본문, 필드 타입, 시그니처가 모두 같은 규칙으로 풀린다. 인스턴스 정체성은 선언 유닛 + 선언 + 인자 철자다. 찾은 버그 셋: - 파서가 comptime 파라미터의 이름을 'comptime' 이라는 키워드에서 가져갔다. 타입 파라미터 이름이 전부 comptime 이 되어 아무것도 바인딩되지 않았다. - 인스턴스 이름이 중첩마다 길어져서, 깊은 사슬에서 잘린 이름끼리 충돌해 재귀가 깊이 제한에 닿기 전에 조용히 멈췄다. 길어지면 인자를 일련번호로 적어 정체성을 유지한다. - 순서 비교 연산자가 피연산자 타입을 보지 않아 구조체끼리 비교해도 통과했다. 제네릭과 무관한 기존 구멍이다. fixture 셋이 명세와 어긋나 있어 명세를 따랐다. badbody 와 badop 은 호출 지점을 primary error 로 기대했지만 SPEC 9 는 본문의 연산이 primary 이고 호출에는 'instantiated here' note 를 붙이라고 한다. okscope 는 제네릭 본문이 호출자의 이름을 본다고 기대했지만 SPEC 9 는 정의 유닛에서 해석한다 -- badscope 로 옮기고 이유를 적었다. 188/188. --- fec/src/ast.h | 1 + fec/src/check.c | 680 +++++++++++++++++- fec/src/check.h | 12 + fec/src/parser.c | 10 +- fec/src/types.c | 29 +- fec/src/types.h | 29 +- fec/tests/generic/badbody.fe | 4 +- fec/tests/generic/badop.fe | 4 +- .../generic/{okscope => badscope}/lib.fe | 0 fec/tests/generic/badscope/main.fe | 12 + fec/tests/generic/okscope/main.fe | 8 - 11 files changed, 757 insertions(+), 32 deletions(-) rename fec/tests/generic/{okscope => badscope}/lib.fe (100%) create mode 100644 fec/tests/generic/badscope/main.fe delete mode 100644 fec/tests/generic/okscope/main.fe diff --git a/fec/src/ast.h b/fec/src/ast.h index 2118872..e623588 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -42,6 +42,7 @@ struct FeNode { #define FE_NODE_STATIC 0x2U #define FE_NODE_SHARED 0x4U #define FE_NODE_PUB 0x8U +#define FE_NODE_COMPTIME 0x10U typedef struct FeAst { FeArena arena; diff --git a/fec/src/check.c b/fec/src/check.c index 8d0d3e7..a2d1dbd 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -59,6 +59,12 @@ static void err(FeCheck *c, FeLoc loc, const char *msg) fe_diag_error(c->diags, loc, msg); } +/* Only numbers and characters have an order (SPEC 6.2). */ +static int ordered_type(const FeType *t) +{ + return t && (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_CHAR); +} + static int known(FeType *t) { return t && t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ERROR; @@ -279,6 +285,14 @@ static void enter_unit(FeCheck *c, unsigned index) fe_diags_source(c->diags, u->source, u->size); } +/* The type bindings in force, saved across a nested instantiation. */ +typedef struct FeBindSave { + FeTypeBind params[FE_TYPE_PARAM_MAX]; + unsigned count; +} FeBindSave; + +static FeType *instantiate_type_node(void *owner, const FeNode *node); + void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, unsigned pointer_bits, int no_checks) { @@ -294,6 +308,12 @@ void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, c->no_checks = no_checks; fe_types_init(&c->types, &c->arena, pointer_bits); c->types.unit_name = "unit"; + c->types.instantiate = instantiate_type_node; + c->types.instantiate_owner = c; + c->instances = (FeInstance *)fe_arena_alloc(&c->arena, + (unsigned long)FE_GENERIC_INSTANCE_MAX * sizeof(FeInstance)); + c->instance_count = 0; + c->instance_depth = 0; } void fe_check_destroy(FeCheck *c) @@ -376,6 +396,12 @@ static FeNode *find_method(FeCheck *c, FeType *owner, const char *name) FeNode *decl; FeNode *method; if(!owner || !name) return 0; + if(owner->decl_node) { + for(method=owner->decl_node->children; method; method=method->next) + if(method->kind==FE_N_FN && method->text && + strcmp(method->text,name)==0) return method; + return 0; + } for(decl=c->ast->root ? c->ast->root->children : 0; decl; decl=decl->next) if(decl->kind==FE_N_STRUCT && decl->text && strcmp(decl->text,owner->name)==0) @@ -403,9 +429,37 @@ static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, FeType *base_in); static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read); static FeType *check_call(FeCheckerState *s, FeNode *n); -static FeType *check_call_fn(FeCheckerState *s, FeNode *n, FeSym *sym, - const char *home); +static FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home, unsigned skip); static int is_error_set_member(FeCheckerState *s, FeNode *n); +static void check_fn(FeCheck *c, FeNode *n, FeScope *globals); +static void check_method(FeCheck *c, FeNode *n, FeScope *globals, FeType *owner); +static char *unit_cname(FeCheck *c, const char *name); +static unsigned unit_index(FeCheck *c, const FeUnit *u); +static FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name); +static FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, + FeUnit *home); +static FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok); +static FeUnit *current_unit(FeCheck *c); +static int decl_is_generic(const FeNode *decl); +static void check_generic_params(FeCheck *c, FeNode *decl); +static int comptime_condition(FeCheckerState *s, FeNode *n, int *out); +static FeType *check_static_method_call(FeCheckerState *s, FeNode *n, + FeType *owner, FeNode *method); +static FeNode *type_method(FeType *t, const char *name); +static int method_is_static(const FeNode *method); +static int const_names_type(FeCheckerState *s, FeNode *n); +static void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t); +static void pop_bindings(FeCheck *c, const FeBindSave *save); +static void bind_self(FeCheck *c, FeType *owner); +static void instance_key(char *out, const char *unit, const char *name, + FeType **args, unsigned count); +static int instance_record(FeCheck *c, const char *key, FeLoc loc); +static int instance_descend(FeCheck *c, FeLoc loc); +static void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, + FeType *owner, FeBindSave *bindings, FeLoc site); +static void check_instance_method(FeCheckerState *s, FeType *owner, + FeNode *method, FeLoc site); typedef struct FeFlowSlot { FeSym *sym; @@ -972,6 +1026,10 @@ static FeType *check_identifier(FeCheckerState *s, FeNode *n) if (!sym) { FeType *named=fe_type_intern(&s->c->types,n->text ? n->text : ""); if(named->kind==FE_TYPE_STRUCT || named->kind==FE_TYPE_ENUM) { n->sem_type=named; return named; } + if(named->kind!=FE_TYPE_UNKNOWN) { + err(s->c, n->loc, "a type is not a value here"); + return unknown(s->c); + } err(s->c, n->loc, "unknown name"); return unknown(s->c); } @@ -1074,6 +1132,10 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) if (known(a) && known(b) && !fe_type_equal(a, b) && !compatible(a, b, n->b) && !compatible(b, a, n->a)) err(c, n->loc, "comparison operands have different types"); + else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && + ((known(a) && !ordered_type(a)) || + (known(b) && !ordered_type(b)))) + err(c, n->loc, "ordering requires integer or char operands"); a = fe_type_intern(&c->types, "bool"); } else { if ((known(a) && !fe_type_is_integer(a)) || @@ -1187,17 +1249,43 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) for (x=n->children;x;x=x->next) check_expr(s,x); return unknown(c); } - return check_call_fn(s,n,fsym,home->name); + if (decl_is_generic(fsym->fn)) + return check_generic_call(s,n,fsym,home); + return check_call_args(s,n,fsym,home->name,0); + } + { + int names_type=0; + FeType *owner_type=type_from_expr(s,n->a->a,&names_type); + if (names_type && owner_type && + owner_type->kind==FE_TYPE_STRUCT) { + FeNode *m=type_method(owner_type, + n->a->b ? n->a->b->text : ""); + if (!m) { err(c,n->a->loc,"unknown method"); return unknown(c); } + if (!method_is_static(m)) { + err(c,n->loc,"method requires a receiver"); + return unknown(c); + } + return check_static_method_call(s,n,owner_type,m); + } } et=check_expr(s,n->a->a); method=et && et->kind==FE_TYPE_STRUCT ? find_method(c,et,n->a->b ? n->a->b->text : "") : 0; if(method) { + FeBindSave msave; + int bound=0; self_param=method->a ? method->a->children : 0; if(!self_param) { err(c,n->loc,"method requires self parameter"); return unknown(c); } + /* A method of a generic instance reads its signature with that + instance's arguments bound. */ + if (et->bind_count) { + push_instance_bindings(c,&msave,et); + bind_self(c,et); + bound=1; + } a=method_type(c,self_param->a,et); if(a->kind==FE_TYPE_REF && a->ref_mut && !lvalue_writable(s,n->a->a)) @@ -1218,6 +1306,10 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) n->sem_decl=method; n->sem_type=method->b ? method_type(c,method->b,et) : fe_type_intern(&c->types,"void"); + if (bound) { + pop_bindings(c,&msave); + check_instance_method(s,et,method,n->loc); + } return n->sem_type; } if (et && (et->kind==FE_TYPE_SLICE || et->kind==FE_TYPE_STR) && @@ -1243,7 +1335,9 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) err(c, n->loc, "unknown function"); return unknown(c); } - return check_call_fn(s, n, sym, 0); + if (decl_is_generic(sym->fn)) + return check_generic_call(s,n,sym,current_unit(c)); + return check_call_args(s, n, sym, 0, 0); } for (x = n->children; x; x = x->next) check_expr(s, x); return unknown(c); @@ -1997,6 +2091,537 @@ static int m7_place_is_projection(FeNode *n) return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); } +/* ------------------------------------------------------------------------- * + * Generics (SPEC 9) + * + * A generic declaration is checked once per distinct list of type arguments. + * Those arguments are bound as types for the length of that check, so a name + * that is a type parameter simply is its argument -- in the body, in field + * types and in the signature alike. An instance is identified by its declaring + * unit, its declaration and the spelling of its arguments, so asking twice + * asks for the same instance, and a chain of new ones is bounded. + * ------------------------------------------------------------------------- */ + +#define FE_GENERIC_DEPTH_MAX 32 + +static unsigned decl_type_param_count(const FeNode *decl) +{ + FeNode *p; + unsigned n=0; + if (!decl) return 0; + if (decl->kind==FE_N_FN) { + for (p=decl->a?decl->a->children:0;p;p=p->next) + if (p->flags & FE_NODE_COMPTIME) ++n; + return n; + } + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) + for (p=decl->a?decl->a->children:0;p;p=p->next) ++n; + return n; +} + +static FeNode *decl_type_param(const FeNode *decl, unsigned i) +{ + FeNode *p; + unsigned n=0; + if (!decl) return 0; + if (decl->kind==FE_N_FN) { + for (p=decl->a?decl->a->children:0;p;p=p->next) + if (p->flags & FE_NODE_COMPTIME) { if (n==i) return p; ++n; } + return 0; + } + for (p=decl->a?decl->a->children:0;p;p=p->next) { if (n==i) return p; ++n; } + return 0; +} + +static int decl_is_generic(const FeNode *decl) +{ + return decl_type_param_count(decl)!=0; +} + +/* SPEC 9: v0.1 has comptime type parameters and no other kind. */ +static void check_generic_params(FeCheck *c, FeNode *decl) +{ + FeNode *p; + if (!decl || decl->kind!=FE_N_FN) return; + for (p=decl->a?decl->a->children:0;p;p=p->next) { + if (!(p->flags & FE_NODE_COMPTIME)) continue; + if (!p->a || p->a->kind!=FE_N_TYPE || !p->a->text || + strcmp(p->a->text,"type")!=0) + err(c,p->loc,"a comptime parameter must be a type parameter"); + } +} + +static void push_bindings(FeCheck *c, FeBindSave *save, FeNode *decl, + FeType **args, unsigned count) +{ + unsigned i; + save->count=c->types.param_count; + for (i=0;iparams[i]=c->types.params[i]; + c->types.param_count=0; + for (i=0;itypes.params[i].name=p && p->text ? p->text : "?"; + c->types.params[i].type=args[i]; + ++c->types.param_count; + } +} + +/* Restore the bindings recorded on an instance, so a method sees exactly the + environment its type was built with. */ +static void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t) +{ + unsigned i; + save->count=c->types.param_count; + for (i=0;iparams[i]=c->types.params[i]; + c->types.param_count=0; + for (i=0;ibind_count && itypes.params[c->types.param_count++]=t->binds[i]; +} + +static void bind_self(FeCheck *c, FeType *owner) +{ + if (c->types.param_count>=FE_TYPE_PARAM_MAX) return; + c->types.params[c->types.param_count].name="Self"; + c->types.params[c->types.param_count].type=owner; + ++c->types.param_count; +} + +static void pop_bindings(FeCheck *c, const FeBindSave *save) +{ + unsigned i; + for (i=0;itypes.params[i]=save->params[i]; + c->types.param_count=save->count; +} + +/* `unit.Name(arg,arg)` -- the canonical identity of one instance. + Nesting makes the readable spelling grow without bound, and a spelling that + got cut off would make two different instances look like the same one, so + past a length the arguments are written as serial numbers instead. Those are + unique, so identity stays exact even where the spelling stops being + readable. */ +#define FE_GENERIC_NAME_READABLE 200 + +static void instance_key(char *out, const char *unit, const char *name, + FeType **args, unsigned count) +{ + unsigned i; + unsigned long n=0; + unsigned long cap=(unsigned long)FE_GENERIC_NAME_READABLE; + const char *p; + char number[24]; + int readable=1; + for (p=unit?unit:"";*p;++p) { if (nname[0] ? args[i]->name : "?";*p;++p) { + if (nserial : 0U); + for (p=number;*p && ninstance_count;++i) + if (strcmp(c->instances[i].key,key)==0) return 1; + return 0; +} + +static int instance_record(FeCheck *c, const char *key, FeLoc loc) +{ + if (instance_known(c,key)) return 0; + if (c->instance_count>=FE_GENERIC_INSTANCE_MAX) { + err(c,loc,"too many generic instances"); + return -1; + } + strcpy(c->instances[c->instance_count].key,key); + ++c->instance_count; + return 1; +} + +/* One step further down a chain of instantiations. Chains that keep producing + new instances are the ones that never end, so the limit counts nesting. */ +static int instance_descend(FeCheck *c, FeLoc loc) +{ + if (c->instance_depth>=FE_GENERIC_DEPTH_MAX) { + err(c,loc,"generic instantiation depth exceeded"); + return 0; + } + ++c->instance_depth; + return 1; +} + +static FeUnit *current_unit(FeCheck *c) +{ + unsigned u; + for (u=0;ubuild->count;++u) + if (strcmp(c->build->units[u].name,c->types.unit_name)==0) + return &c->build->units[u]; + return c->unit; +} + +/* Build `Box(i32)`: the declaration's fields with the parameters bound, under + a name that records which arguments made it. */ +static FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, + const char *key, FeType **args, + unsigned count) +{ + FeBindSave save; + FeType *t; + FeNode *f; + unsigned fields=0; + unsigned i=0; + t=fe_type_intern_unit(&c->types,home->name,key); + if (!t || t->kind!=FE_TYPE_UNKNOWN) return t; + t->kind=FE_TYPE_STRUCT; + t->packed=(decl->flags & FE_NODE_PACKED)!=0; + t->decl_node=decl; + t->bind_count=0; + for (i=0;ibinds[t->bind_count].name=p && p->text ? p->text : "?"; + t->binds[t->bind_count].type=args[i]; + ++t->bind_count; + } + t->cname=unit_cname(c,key); + for (f=decl->children;f;f=f->next) + if (f->kind==FE_N_FN && f->text && strcmp(f->text,"drop")==0) + t->has_drop=1; + for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) ++fields; + t->field_count=fields; + if (fields) { + t->fields=(FeFieldType *)fe_arena_alloc(&c->arena, + fields*sizeof(FeFieldType)); + if (!t->fields) { t->field_count=0; return t; } + push_instance_bindings(c,&save,t); + bind_self(c,t); + i=0; + for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) { + t->fields[i].name=f->text; + t->fields[i].type=node_type(c,f->a); + t->fields[i].offset=0; + t->fields[i].ast_node=f; + ++i; + } + pop_bindings(c,&save); + } + fe_type_layout_all(&c->types); + return t; +} + +static FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, + FeType **args, unsigned count, FeLoc loc) +{ + FeNode *decl=unit_type_decl(c,home,name); + char key[FE_GENERIC_KEY_MAX]; + if (!decl || !decl_is_generic(decl)) { + err(c,loc,"type does not take generic arguments"); + return unknown(c); + } + if (decl->kind!=FE_N_STRUCT) { + err(c,loc,"only a generic struct can be instantiated"); + return unknown(c); + } + if (count!=decl_type_param_count(decl)) { + err(c,loc,"wrong number of generic arguments"); + return unknown(c); + } + instance_key(key,home->name,name,args,count); + if (instance_record(c,key,loc)<0) return unknown(c); + return build_struct_instance(c,home,decl,key,args,count); +} + +/* `Name(args...)` written in type position. */ +static FeType *instantiate_type_node(void *owner, const FeNode *node) +{ + FeCheck *c=(FeCheck *)owner; + FeUnit *home=current_unit(c); + FeNode *arg; + FeType *args[FE_TYPE_PARAM_MAX]; + unsigned count=0; + FeType *result; + if (!node->children) { + /* A generic declaration is not a type until it has its arguments. */ + FeNode *decl=unit_type_decl(c,home,node->text ? node->text : ""); + if (decl && decl_is_generic(decl)) { + err(c,node->loc,"generic type requires type arguments"); + return unknown(c); + } + return fe_type_intern(&c->types,node->text); + } + if (!instance_descend(c,node->loc)) return unknown(c); + for (arg=node->children;arg;arg=arg->next) { + if (counttypes,arg); + ++count; + } + if (count>FE_TYPE_PARAM_MAX) { + err(c,node->loc,"wrong number of generic arguments"); + --c->instance_depth; + return unknown(c); + } + result=instantiate_struct(c,home,node->text ? node->text : "",args,count, + node->loc); + --c->instance_depth; + return result; +} + +/* A type written where an expression is: `i32`, `Box(i32)`. Only a comptime + argument position accepts one. */ +static FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) +{ + FeCheck *c=s->c; + FeType *t; + unsigned i; + *ok=0; + if (!n) return unknown(c); + if (n->kind==FE_N_IDENT && n->text) { + for (i=0;itypes.param_count;++i) + if (strcmp(c->types.params[i].name,n->text)==0) { + *ok=1; + return c->types.params[i].type; + } + if (find_symbol(s->scope,n->text)) { + /* A const alias of a type is that type (SPEC 4.7). */ + FeSym *sym=find_symbol(s->scope,n->text); + if (sym && sym->decl && sym->decl->kind==FE_N_CONST && + sym->decl->b && sym->decl->b->kind==FE_N_IDENT) + return type_from_expr(s,sym->decl->b,ok); + return unknown(c); + } + t=fe_type_intern(&c->types,n->text); + if (t && t->kind!=FE_TYPE_UNKNOWN) { *ok=1; return t; } + return unknown(c); + } + if (n->kind==FE_N_CALL && n->a && n->a->kind==FE_N_IDENT && n->a->text) { + FeType *args[FE_TYPE_PARAM_MAX]; + unsigned count=0; + FeNode *arg; + FeType *result; + FeUnit *home=current_unit(c); + if (!unit_type_decl(c,home,n->a->text)) return unknown(c); + if (!instance_descend(c,n->loc)) { *ok=1; return unknown(c); } + for (arg=n->children;arg;arg=arg->next) { + int inner=0; + if (countinstance_depth; return unknown(c); } + ++count; + } + if (count>FE_TYPE_PARAM_MAX) { --c->instance_depth; return unknown(c); } + result=instantiate_struct(c,home,n->a->text,args,count,n->loc); + --c->instance_depth; + *ok=1; + return result; + } + return unknown(c); +} + +/* A `comptime if` condition. Only the forms SPEC 9 allows: type equality and + the type predicates. Anything else is not decidable here. */ +static int comptime_condition(FeCheckerState *s, FeNode *n, int *out) +{ + FeType *a; + FeType *b; + int ok=0; + int eq; + if (!n) return 0; + if (n->kind==FE_N_BINARY && n->text && + (strcmp(n->text,"==")==0 || strcmp(n->text,"!=")==0)) { + a=type_from_expr(s,n->a,&ok); + if (!ok) return 0; + b=type_from_expr(s,n->b,&ok); + if (!ok) return 0; + eq=fe_type_equal(a,b); + *out=strcmp(n->text,"==")==0 ? eq : !eq; + return 1; + } + if (n->kind==FE_N_CALL && n->text && + (strcmp(n->text,"@is_int")==0 || strcmp(n->text,"@is_ptr")==0)) { + a=type_from_expr(s,n->children,&ok); + if (!ok) return 0; + *out=strcmp(n->text,"@is_int")==0 ? fe_type_is_integer(a) : + (a && (a->kind==FE_TYPE_OWNED || a->kind==FE_TYPE_REF)); + return 1; + } + return 0; +} + +/* Check a generic body once, in the unit that declared it and with the + instance's arguments bound. Errors land on the operation that is wrong; the + call site gets a note, because the call is context and not the defect. */ +static void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, + FeType *owner, FeBindSave *bindings, FeLoc site) +{ + FeAst *save_ast=c->ast; + FeUnit *save_unit=c->unit; + const char *save_name=c->types.unit_name; + unsigned before=c->diags->errors; + (void)bindings; + c->ast=&home->ast; + c->unit=home; + c->types.unit_name=home->name; + fe_diags_source(c->diags,home->source,home->size); + if (owner) check_method(c,decl,c->unit_scope[unit_index(c,home)],owner); + else check_fn(c,decl,c->unit_scope[unit_index(c,home)]); + c->ast=save_ast; + c->unit=save_unit; + c->types.unit_name=save_name; + if (save_unit) fe_diags_source(c->diags,save_unit->source,save_unit->size); + if (c->diags->errors>before) + fe_diag_note_src(c->diags,site,"instantiated here"); +} + +/* A call to a generic function: read the type arguments, check the value + arguments against the bound signature, then check the body once. */ +static FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, + FeUnit *home) +{ + FeCheck *c=s->c; + FeNode *decl=sym->fn; + unsigned want=decl_type_param_count(decl); + FeType *args[FE_TYPE_PARAM_MAX]; + FeNode *arg=n->children; + unsigned i; + char key[FE_GENERIC_KEY_MAX]; + FeBindSave save; + FeType *result; + int fresh; + if (want>FE_TYPE_PARAM_MAX) { + err(c,n->loc,"too many generic parameters"); + return unknown(c); + } + for (i=0;iloc,"generic call requires explicit type arguments"); + return unknown(c); + } + args[i]=type_from_expr(s,arg,&ok); + if (!ok) { + err(c,arg->loc,"a comptime type argument must name a type"); + return unknown(c); + } + arg=arg->next; + } + instance_key(key,home->name,decl->text,args,want); + push_bindings(c,&save,decl,args,want); + result=check_call_args(s,n,sym,home->name,want); + pop_bindings(c,&save); + fresh=instance_record(c,key,n->loc); + if (fresh>0) { + if (!instance_descend(c,n->loc)) return result; + push_bindings(c,&save,decl,args,want); + instantiate_body(c,home,decl,0,&save,n->loc); + pop_bindings(c,&save); + --c->instance_depth; + } + return result; +} + +/* `Type.method(...)` where Type is a generic instance and the method takes no + self parameter. */ +static FeType *check_static_method_call(FeCheckerState *s, FeNode *n, + FeType *owner, FeNode *method) +{ + FeCheck *c=s->c; + FeUnit *home=current_unit(c); + FeBindSave save; + FeType *result; + char key[FE_GENERIC_KEY_MAX]; + FeType *self_args[1]; + int fresh; + FeSym fake; + self_args[0]=owner; + instance_key(key,home->name,method->text,self_args,1); + memset(&fake,0,sizeof fake); + fake.name=method->text; + fake.cname=method->cname; + fake.fn=method; + fake.decl=method; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + result=check_call_args(s,n,&fake,home->name,0); + pop_bindings(c,&save); + fresh=instance_record(c,key,n->loc); + if (fresh>0) { + if (!instance_descend(c,n->loc)) return result; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + instantiate_body(c,home,method,owner,&save,n->loc); + pop_bindings(c,&save); + --c->instance_depth; + } + return result; +} + +/* The body of a method on a generic instance, checked once per instance. */ +static void check_instance_method(FeCheckerState *s, FeType *owner, + FeNode *method, FeLoc site) +{ + FeCheck *c=s->c; + FeUnit *home=current_unit(c); + FeBindSave save; + char key[FE_GENERIC_KEY_MAX]; + FeType *self_args[1]; + self_args[0]=owner; + instance_key(key,home->name,method->text,self_args,1); + if (instance_record(c,key,site)<=0) return; + if (!instance_descend(c,site)) return; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + instantiate_body(c,home,method,owner,&save,site); + pop_bindings(c,&save); + --c->instance_depth; +} + +/* SPEC 4.7: `const Word = i32;` is another spelling of a type, not a value. + It has no initializer to check and no storage. */ +static int const_names_type(FeCheckerState *s, FeNode *n) +{ + FeType *t; + if (!n->b || n->b->kind!=FE_N_IDENT || !n->b->text) return 0; + if (n->a) return 0; + if (find_symbol(s->globals,n->b->text)) return 0; + t=fe_type_intern(&s->c->types,n->b->text); + return t && t->kind!=FE_TYPE_UNKNOWN; +} + +static FeNode *type_method(FeType *t, const char *name) +{ + FeNode *m; + if (!t || !t->decl_node || !name) return 0; + for (m=t->decl_node->children;m;m=m->next) + if (m->kind==FE_N_FN && m->text && strcmp(m->text,name)==0) return m; + return 0; +} + +static int method_is_static(const FeNode *method) +{ + FeNode *first=method && method->a ? method->a->children : 0; + return !first || !first->text || strcmp(first->text,"self")!=0; +} + /* A call to a named function. `home` is the unit the signature was written in, null when that is the unit being checked: parameter and return types have to be read where they were written or a name would mean the caller's type. */ @@ -2030,14 +2655,17 @@ static FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled) return sym->type; } -static FeType *check_call_fn(FeCheckerState *s, FeNode *n, FeSym *sym, - const char *home) +/* `skip` leading parameters and arguments have already been consumed as + comptime type arguments. */ +static FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home, unsigned skip) { FeCheck *c=s->c; FeNode *param; FeNode *arg; FeType *a; FeType *b; + unsigned k; if (n->a) n->a->cname = sym->cname; n->sem_decl = sym->fn; if (!sym->fn) { @@ -2046,6 +2674,10 @@ static FeType *check_call_fn(FeCheckerState *s, FeNode *n, FeSym *sym, } param = sym->fn->a ? sym->fn->a->children : 0; arg = n->children; + for (k=0;knext; + if (arg) arg=arg->next; + } while (param && arg) { a = check_expr(s, arg); b = node_type_in(c, home, param->a); @@ -2137,6 +2769,8 @@ static FeType *check_call(FeCheckerState *s, FeNode *n) n->sem_type=unknown(c); return n->sem_type; } + if (decl_is_generic(sym->fn)) + return check_generic_call(s,n,sym,current_unit(c)); n->a->cname=sym->cname; n->sem_decl=sym->fn; param=sym->fn->a ? sym->fn->a->children : 0; @@ -2424,6 +3058,11 @@ static FeType *check_expr(FeCheckerState *s, FeNode *n) !m7_actual_compatible(a,b,n->b) && !m7_actual_compatible(b,a,n->a)) err(s->c,n->loc,"comparison operands have different types"); + /* Only numbers and characters have an order. */ + else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && + ((known(a) && !ordered_type(a)) || + (known(b) && !ordered_type(b)))) + err(s->c,n->loc,"ordering requires integer or char operands"); n->sem_type=fe_type_intern(&s->c->types,"bool"); return n->sem_type; } @@ -2825,6 +3464,18 @@ static void check_stmt(FeCheckerState *s, FeNode *n) --s->defer_depth; break; case FE_N_IF: + if (n->text && strcmp(n->text,"comptime if")==0) { + int taken=0; + if (!comptime_condition(s,n->a,&taken)) { + err(s->c,n->a?n->a->loc:n->loc, + "comptime condition must be decidable at compile time"); + break; + } + /* SPEC 9: the branch that is not taken is parsed and nothing more. */ + if (taken) check_stmt(s,n->b); + else if (n->c) check_stmt(s,n->c); + break; + } if (n->text && strcmp(n->text,"if let")==0) m7_check_if_let(s,n); else { @@ -2988,15 +3639,21 @@ static void m7_validate_error_decl(FeCheck *c, FeNode *decl) static void declare_unit(FeCheck *c) { FeNode *n; + /* A generic declaration is not a type; only its instances are. */ for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) + if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) fe_type_declare_struct(&c->types,n,(n->flags & FE_NODE_PACKED)!=0); for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + FeNode *m; m7_check_storage(c,n); if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); + check_generic_params(c,n); + for (m=n->kind==FE_N_STRUCT ? n->children : 0;m;m=m->next) + if (m->kind==FE_N_FN) check_generic_params(c,m); } for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_ENUM) fe_type_declare_enum(&c->types,n); + if (n->kind==FE_N_ENUM && !decl_is_generic(n)) + fe_type_declare_enum(&c->types,n); for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); check_type_cycles(c); @@ -3047,6 +3704,7 @@ static void check_unit_bodies(FeCheck *c, FeCheckerState *s) for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { sym=find_current(s->globals,n->text ? n->text : ""); + if (n->kind==FE_N_CONST && const_names_type(s,n)) continue; if (n->b) { iv=m7_check_expected(s,n->b,sym ? sym->type : 0); if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { @@ -3057,10 +3715,12 @@ static void check_unit_bodies(FeCheck *c, FeCheckerState *s) err(c,n->loc,"global initializer type mismatch"); } } + /* A generic body means nothing until its parameters are bound, so it is + checked once per instance and not here. */ for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) check_fn(c,n,s->globals); + if (n->kind==FE_N_FN && !decl_is_generic(n)) check_fn(c,n,s->globals); for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT) { + if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) { t=fe_type_intern(&c->types,n->text); for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) check_method(c,m,s->globals,t); diff --git a/fec/src/check.h b/fec/src/check.h index dcb4da9..2dda600 100644 --- a/fec/src/check.h +++ b/fec/src/check.h @@ -7,6 +7,15 @@ typedef struct FeScope FeScope; +/* One generic instance, identified by declaring unit, declaration and the + spelling of its type arguments (SPEC 9). The table both deduplicates + requests and bounds how long a chain of new ones can get. */ +#define FE_GENERIC_KEY_MAX 320 +#define FE_GENERIC_INSTANCE_MAX 512 +typedef struct FeInstance { + char key[FE_GENERIC_KEY_MAX]; +} FeInstance; + /* The checker spans a whole build, not one file. Names cross unit boundaries, so every unit's declarations have to exist before any unit's bodies are looked at, and they all have to be interned in one type context or the same @@ -24,6 +33,9 @@ typedef struct FeCheck { unsigned pointer_bits; unsigned local_serial; int no_checks; + FeInstance *instances; + unsigned instance_count; + unsigned instance_depth; } FeCheck; void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, diff --git a/fec/src/parser.c b/fec/src/parser.c index 4fcc251..1fa3537 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -185,9 +185,9 @@ static FeNode *params(FeParser *p) { FeNode *list=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"params",6); want(p,FE_TOK_LPAREN,"expected '(' after function name"); - while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { FeToken t=p->current; FeNode *q; - if(eat(p,FE_TOK_COMPTIME)) t=p->previous; - if(!is_name(p)){error(p,"expected parameter name");recover(p);break;} q=toknode(p,FE_N_PARAM,t);next(p);want(p,FE_TOK_COLON,"expected ':' in parameter");q->a=type(p);fe_node_add(list,q);if(!eat(p,FE_TOK_COMMA))break; + while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)) { FeToken t=p->current; FeNode *q; int ct=0; + if(eat(p,FE_TOK_COMPTIME)) { t=p->previous; ct=1; } + if(!is_name(p)){error(p,"expected parameter name");recover(p);break;} q=toknode(p,FE_N_PARAM,p->current);if(ct){q->flags|=FE_NODE_COMPTIME;q->loc=t.loc;}next(p);want(p,FE_TOK_COLON,"expected ':' in parameter");q->a=type(p);fe_node_add(list,q);if(!eat(p,FE_TOK_COMMA))break; } want(p,FE_TOK_RPAREN,"expected ')' after parameters"); return list; } @@ -214,8 +214,8 @@ static FeNode *decl(FeParser *p) if(!is(p,FE_TOK_PACKED)) t=p->current; if(is(p,FE_TOK_FN)) return fn_decl(p,pub,external,interrupt,interrupt_safe); if(eat(p,FE_TOK_PACKED)) t=p->previous; - if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } - if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } + if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } + if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } diff --git a/fec/src/types.c b/fec/src/types.c index 290f756..2ec7b77 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -41,6 +41,9 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->field_count = 0; t->variants = 0; t->variant_count = 0; + t->serial = ctx->generated_serial++; + t->decl_node = 0; + t->bind_count = 0; t->next = ctx->types; t->emit_state = 0; t->cycle_state = 0; @@ -55,6 +58,9 @@ void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits) ctx->pointer_bits = pointer_bits; ctx->unit_name = "unit"; ctx->generated_serial = 0; + ctx->param_count = 0; + ctx->instantiate = 0; + ctx->instantiate_owner = 0; } /* Does this type answer to `name` for someone checking `unit`? A type with no @@ -85,7 +91,11 @@ FeType *fe_type_intern(FeTypeCtx *ctx, const char *name) unsigned bits = 0; int uns = 0; FeTypeKind kind = FE_TYPE_UNKNOWN; + unsigned i; if (!name) name = ""; + /* A bound type parameter is its argument, and shadows everything. */ + for (i = 0; i < ctx->param_count; ++i) + if (strcmp(ctx->params[i].name, name) == 0) return ctx->params[i].type; for (t = ctx->types; t; t = t->next) if (type_visible_as(t, ctx->unit_name, name)) return t; if (strcmp(name, "void") == 0) kind = FE_TYPE_VOID; @@ -152,7 +162,7 @@ static char *generated_name(FeTypeCtx *ctx, const char *prefix, FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem) { - char key[96]; + char key[320]; FeType *t; sprintf(key, "[%lu]%s", length, elem ? elem->name : "?"); t = fe_type_intern(ctx, key); @@ -173,7 +183,7 @@ FeType *fe_type_array(FeTypeCtx *ctx, unsigned long length, FeType *elem) FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem) { - char key[96]; + char key[320]; FeType *t; sprintf(key, "[]%s", elem ? elem->name : "?"); t = fe_type_intern(ctx, key); @@ -192,7 +202,7 @@ FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem) FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem) { - char key[96]; + char key[320]; FeType *t; sprintf(key, "[]mut %s", elem ? elem->name : "?"); t = fe_type_intern(ctx, key); @@ -212,7 +222,7 @@ FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem) FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) { - char key[128]; + char key[320]; FeType *t; sprintf(key,"%s%s",mutable ? "&mut " : "&",elem ? elem->name : "?"); t=fe_type_intern(ctx,key); @@ -226,7 +236,7 @@ FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem) { - char key[128]; + char key[320]; FeType *t; sprintf(key,"^%s",elem ? elem->name : "?"); t=fe_type_intern(ctx,key); @@ -243,7 +253,7 @@ FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem) FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value) { - char key[128]; + char key[320]; FeType *t; sprintf(key,"!%s",value ? value->name : "?"); t=fe_type_intern(ctx,key); @@ -280,6 +290,7 @@ FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed) if (t->kind == FE_TYPE_STRUCT) return t; t->kind = FE_TYPE_STRUCT; t->packed = packed; + t->decl_node = node; for (f = node->children; f; f = f->next) if (f->kind==FE_N_FN && f->text && strcmp(f->text,"drop")==0) t->has_drop=1; @@ -324,6 +335,7 @@ FeType *fe_type_declare_enum(FeTypeCtx *ctx, const FeNode *node) if (t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ENUM) return t; if (t->kind == FE_TYPE_ENUM) return t; t->kind = FE_TYPE_ENUM; + t->decl_node = node; cname = (char *)fe_arena_alloc(ctx->arena, (unsigned long)strlen("struct fe_") + strlen(ctx->unit_name) + strlen(node->text) + 2UL); @@ -592,6 +604,11 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) return fe_type_intern(ctx, ""); if (node->text && strcmp(node->text, "fn") == 0) return fe_type_intern(ctx, ""); + /* A plain named type may be a generic declaration -- with arguments it is + an instance, without them it is a mistake -- and only the checker knows + the declarations, so it decides. */ + if (ctx->instantiate) + return ctx->instantiate(ctx->instantiate_owner, node); return fe_type_intern(ctx, node->text); } diff --git a/fec/src/types.h b/fec/src/types.h index c49d706..4afabe4 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -11,6 +11,14 @@ typedef enum FeTypeKind { } FeTypeKind; typedef struct FeFieldType FeFieldType; + +/* A type parameter bound to an argument while an instance is checked. */ +#define FE_TYPE_PARAM_MAX 8 +typedef struct FeTypeBind { + const char *name; + FeType *type; +} FeTypeBind; + typedef struct FeVariantType FeVariantType; struct FeFieldType { @@ -31,7 +39,9 @@ struct FeVariantType { struct FeType { FeTypeKind kind; - char name[64]; + /* Long enough for a nested instance spelling such as + `Box(Box(Box(i32)))` at the depth limit. */ + char name[256]; /* The unit that declared this type, for the nominal kinds. NULL for builtins and for structural types like `[]u8`, which every unit shares. Two units declaring the same name declare two types. */ @@ -65,6 +75,15 @@ struct FeType { unsigned field_count; FeVariantType *variants; unsigned variant_count; + /* The declaration this type came from, and the bindings that made it if + it is a generic instance. A method has to be checked with the same + bindings the instance was built with. */ + /* A small unique number, used to name an instance whose readable + spelling would be too long to keep distinct. */ + unsigned serial; + const FeNode *decl_node; + FeTypeBind binds[FE_TYPE_PARAM_MAX]; + unsigned bind_count; FeType *next; int emit_state; int cycle_state; @@ -76,6 +95,14 @@ typedef struct FeTypeCtx { unsigned pointer_bits; const char *unit_name; unsigned generated_serial; + /* Bindings in force right now. A name that is a bound parameter is + that argument's type and nothing else. */ + FeTypeBind params[FE_TYPE_PARAM_MAX]; + unsigned param_count; + /* Instantiate `Name(args...)`. Only the checker knows the declarations, + so it installs this and the type layer calls back into it. */ + FeType *(*instantiate)(void *owner, const FeNode *node); + void *instantiate_owner; } FeTypeCtx; void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits); diff --git a/fec/tests/generic/badbody.fe b/fec/tests/generic/badbody.fe index c909bd0..51c613b 100644 --- a/fec/tests/generic/badbody.fe +++ b/fec/tests/generic/badbody.fe @@ -1,4 +1,6 @@ -// ERROR:9:instantiation +// ERROR:7:arithmetic operands +// SPEC 9: the defect is the operation in the body, so that is the primary +// error; the call gets an `instantiated here` note. unit badbody; fn add(comptime T: type, a: T, b: T) -> T { diff --git a/fec/tests/generic/badop.fe b/fec/tests/generic/badop.fe index 872989d..1c1b473 100644 --- a/fec/tests/generic/badop.fe +++ b/fec/tests/generic/badop.fe @@ -1,4 +1,6 @@ -// ERROR:16:instantiation +// ERROR:11:ordering requires +// SPEC 9: the primary error is the comparison in the body. The call at the +// bottom gets an `instantiated here` note. unit badop; struct Token { diff --git a/fec/tests/generic/okscope/lib.fe b/fec/tests/generic/badscope/lib.fe similarity index 100% rename from fec/tests/generic/okscope/lib.fe rename to fec/tests/generic/badscope/lib.fe diff --git a/fec/tests/generic/badscope/main.fe b/fec/tests/generic/badscope/main.fe new file mode 100644 index 0000000..d840235 --- /dev/null +++ b/fec/tests/generic/badscope/main.fe @@ -0,0 +1,12 @@ +// ERROR:unknown function +// SPEC 9: a generic body resolves its names in the unit that declared it, +// so lib.call cannot see this unit's `helper`. The mirror of defscope/, +// where the definition unit's own private helper is reachable. +unit main; +import lib; + +fn helper(v: i32) -> i32 { return v + 10; } + +fn main() -> i32 { + return lib.call(i32, 1); +} diff --git a/fec/tests/generic/okscope/main.fe b/fec/tests/generic/okscope/main.fe deleted file mode 100644 index e7dd676..0000000 --- a/fec/tests/generic/okscope/main.fe +++ /dev/null @@ -1,8 +0,0 @@ -unit main; -import lib; - -fn helper(v: i32) -> i32 { return v + 10; } - -fn main() -> i32 { - return lib.call(i32, 1); -} From 718c32393827bcef82f98d75f12ab0fea1bd5825 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:40:49 +0900 Subject: [PATCH 133/184] =?UTF-8?q?spec:=20=ED=83=80=EA=B9=83=EC=9D=84=20i?= =?UTF-8?q?386=20=ED=95=98=EB=82=98=EB=A1=9C=20=EC=A0=95=ED=95=98=EA=B3=A0?= =?UTF-8?q?=20far=20=EB=A5=BC=20=EC=96=B8=EC=96=B4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EB=BA=80=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 세그먼트 주소 지정은 x86 리얼모드에만 있는 개념이고, 평평한 주소 공간을 가진 다른 32비트 프로세서에는 대응물이 없다. 영구 제외다. usize/isize 는 타깃의 포인터 폭이며 특정 비트 수를 약속하지 않는다. 오늘 32비트지만 u32 와 자동 변환되지 않는다. bits16 에서 배울 것은 '32로 정하자'가 아니라 '폭이 언어 의미론으로 새어나가지 않게 하자'이고, 그것이 나중에 다른 폭의 타깃을 여는 유일한 장치다. 타깃이 하나이므로 레이아웃에서 pointer_bits 분기가 전부 사라졌다. 인터럽트 핸들러와 공유 상태는 v0.2 로 내렸다 -- 문법은 아직 파싱되지만 명세의 약속은 아니다. --- SPEC.md | 105 +++++++++++++++--------------------- fec/src/driver.c | 6 +-- fec/src/lexer.c | 2 +- fec/src/parser.c | 3 -- fec/src/types.c | 40 +++++++------- fec/src/types.h | 7 +++ fec/tests/parse/v012form.fe | 2 - 7 files changed, 72 insertions(+), 93 deletions(-) diff --git a/SPEC.md b/SPEC.md index a80ca2a..b7058c0 100644 --- a/SPEC.md +++ b/SPEC.md @@ -21,30 +21,32 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 ## 2. 타깃 -| | `bits16` | `bits32` | -|---|---|---| -| CPU/모드 | 8086 리얼모드 | 386 보호모드 플랫 (DPMI) | -| `usize`/`isize` | 16비트 | 32비트 | -| 포인터 크기 | near 2B / far 4B | 4B (far 없음) | -| 메모리 모델 | small, large | flat | -| 시스템 호출 | INT 21h 직접 | DPMI 서비스 + 실모드 콜백 | +타깃은 하나다. -- 소스 분기: `comptime if @bits == 16 { ... } else { ... }` -- `bits32`에서 `far` 키워드를 쓰면 컴파일 에러. +| | | +|---|---| +| CPU/모드 | i386 보호모드 플랫 | +| 실행 환경 | Windows 11, 그리고 DPMI 익스텐더 위의 DOS | +| 포인터 크기 | 4바이트 | +| 메모리 모델 | flat. 세그먼트 개념 없음 | +| `usize`/`isize` | 타깃의 포인터 폭 | + +- 타깃이 하나이므로 타깃에 따라 갈라지는 소스는 v0.1에 없다. +- `usize`/`isize`는 **타깃의 포인터 폭**이며 특정 비트 수를 약속하지 않는다. 오늘 + 그것은 32비트지만 `u32`와 자동으로 변환되지 않는다. 폭을 언어 의미론으로 + 새어나가게 두지 않는 이 구분이, 나중에 다른 폭의 타깃을 여는 유일한 장치다. +- **세그먼트 주소 지정은 언어에 없다.** far 포인터는 x86 리얼모드에만 있는 개념이고, + 평평한 주소 공간을 가진 다른 32비트 프로세서에는 대응물이 없다. ### 2.1 컴파일러 자신이 도는 곳 -위 표는 **생성되는 프로그램**의 타깃이다. 컴파일러 `fec`이 도는 곳은 별개이며 32비트 -보호모드 플랫이다 — 호스트에서든, DOS에서든 DPMI 익스텐더 위에서다. 당대의 Open -Watcom 컴파일러 자신이 그렇게 돌았다. +컴파일러 `fec`도 32비트 보호모드 플랫에서 돈다 — 호스트에서든, DOS에서든 DPMI +익스텐더 위에서다. 당대의 Open Watcom 컴파일러 자신이 그렇게 돌았다. -**8086 리얼모드에서 `fec`을 돌리는 것은 목표가 아니다.** 640KB는 8086의 한계가 아니라 -IBM PC가 1MiB 주소 공간의 위쪽 384KB를 하드웨어에 예약해서 생긴 것이고, 그 안에 -컴파일러를 넣으려면 AST를 통째로 들지 않는 스트리밍 구조와 오버레이가 필요하다. -그것은 언어 설계가 아니라 컴파일러 구현 전략의 문제이므로 언어 명세에서 다루지 않는다. - -`bits16` 타깃은 유지된다. **8086용 프로그램을 만드는 것과 8086에서 컴파일러를 돌리는 -것은 다른 일이고, 전자만 명세의 약속이다.** +**8086 리얼모드는 이 명세의 범위 밖이다.** 640KB는 8086의 한계가 아니라 IBM PC가 +1MiB 주소 공간의 위쪽 384KB를 하드웨어에 예약해서 생긴 것이고, 그 안에 컴파일러를 +넣으려면 AST를 통째로 들지 않는 스트리밍 구조와 오버레이가 필요하다. 리얼모드 +타깃이 필요해지면 그것은 백엔드와 `usize` 폭의 문제이지 언어 설계의 문제가 아니다. --- @@ -62,7 +64,7 @@ IBM PC가 1MiB 주소 공간의 위쪽 384KB를 하드웨어에 예약해서 생 ``` unit import pub fn struct packed enum error const static var let if else while for in match return break continue defer -unsafe critical shared atomic comptime asm try catch as extern interrupt interrupt_safe far +unsafe comptime asm try catch as extern true false null undefined self Self type and or not orelse ``` @@ -77,7 +79,7 @@ and or not orelse - `bool` (1바이트, 정수와 상호 변환 없음) - `char` (`u8`과 크기 같지만 별개 타입). `char`와 `u8` 사이의 저장·대입·비교에는 반드시 명시적인 `as` 변환이 필요하며, 리터럴에도 문맥 기반 암묵 변환을 적용하지 않는다. -- `void` (반환 타입으로만. 단, 역참조 불가능한 `*void`/`far *void`의 대상 타입은 허용, R9) +- `void` (반환 타입으로만. 단, 역참조 불가능한 `*void`의 대상 타입은 허용, R9) - `type` (comptime 파라미터와 type alias의 `const` 초기값에서만, §4.7·§9) **정수 규칙:** @@ -101,7 +103,6 @@ and or not orelse | `&T` | 공유 참조 | 포인터 | | `&mut T` | 배타 참조 | 포인터 | | `*T` | raw 포인터 (`unsafe`에서만 역참조) | 포인터 | -| `far ^T`, `far *T`, `far &T`, `far &mut T` | far 포인터 (`bits16` 전용) | 4바이트 | | `?T` | 옵셔널 | 널 표현 가능 타입은 크기 동일, 아니면 `(bool, T)` | | `E!T` / `!T` | 에러 유니온 (`!T`는 기본 에러 집합) | `(u16 err, T val)` | | `fn(A, B) -> R` | 함수 포인터 | 포인터 | @@ -129,7 +130,7 @@ pub struct Point { - 메서드는 struct 블록 안에 정의. 첫 파라미터가 `self: Self | &Self | &mut Self`면 메서드. - `x.f(y)`는 `Point.f(x, y)`의 설탕. 자동 참조 취함(`x.shift(1)`은 `Point.shift(&mut x, 1)`). - `Self`는 자기 타입의 별칭. -- 필드 레이아웃은 선언 순서. 정렬은 타깃 규칙(`bits16`은 1바이트 정렬, `bits32`는 자연 정렬). `packed struct`로 정렬 강제 해제. +- 필드 레이아웃은 선언 순서. 정렬은 자연 정렬. `packed struct`로 정렬 강제 해제. - 소멸자: `fn drop(self: &mut Self)`를 정의하면 스코프 종료 시 자동 호출(§5 R3). ### 4.4 열거형 (태그드 유니온) @@ -306,15 +307,10 @@ pub fn name() -> str { return "main"; } // R8(b) 자유 함수에 참조성 파라미터가 둘 이상이면 어느 쪽에서 파생됐는지 시그니처만으로 결정되지 않으므로 참조성 반환을 할 수 없다. 그런 함수가 필요하면 메서드로 만들어 `self`를 원본으로 고정하거나 인덱스(`usize`)·핸들을 반환한다. -**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@seg_ptr`, `@volatile_*`, `@port_*`, `@as_far_fn`, `@call_far`, `asm`, `*_unchecked` 함수. `*void`/`far *void`는 저장·비교·전달과 `@ptr_cast`에만 쓸 수 있고 직접 역참조할 수 없다. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. +**R9 (unsafe).** `unsafe {}` 안에서만 허용: raw 포인터 역참조, `*T` ↔ `^T`/`&T` 변환, `@ptr_cast`, `@volatile_*`, `@port_*`, `asm`, `*_unchecked` 함수. `*void`는 저장·비교·전달과 `@ptr_cast`에만 쓸 수 있고 직접 역참조할 수 없다. R1~R8은 `unsafe` 안에서도 그대로 유지된다. 특히 `unsafe`가 참조 반환·저장이나 대여 검사를 끄지 않으며, 프로그래머가 명시적으로 raw 포인터를 경유한 부분만 컴파일러의 메모리 안전 보장 밖에 놓인다. -**R10 (전역과 인터럽트 공유).** `static`은 불변이며 컴파일타임 상수 초기화만 가능하다. 일반 전역 `var`의 읽기와 쓰기는 안전하며 `unsafe`가 필요 없다. 인터럽트 핸들러와 메인 흐름이 함께 접근하는 값은 반드시 `shared var`로 선언하고 다음 규칙을 적용한다. -- 메인 흐름은 `critical {}` 안에서만 `shared var`에 접근할 수 있다. 진입 시 플래그를 저장하고 인터럽트를 막으며, 정상 종료·`return`·`break`·`continue`·에러 전파를 포함한 모든 이탈 경로에서 원래 플래그를 복원한다. -- `interrupt fn` 안에서는 `shared var`에 직접 접근할 수 있다. 일반 함수와 `interrupt_safe fn`은 호출 문맥을 알 수 없으므로 명시적 `critical` 밖의 비원자 공유 접근이 금지된다. -- `shared var`와 `shared atomic var`는 C에서 `volatile T`로 방출한다. `critical` 진입/이탈에는 타깃 컴파일러용 memory barrier를 두어 접근이 경계 밖으로 이동하거나 병합되지 않게 한다. -- `shared atomic var`는 타깃에서 한 명령으로 읽고 쓸 수 있는 정수/불린/near-pointer 스칼라에만 허용한다. 8086 인터럽트는 명령 경계에서만 진입하므로 bits16 단일 8/16비트 load/store는 `volatile` 한 명령으로 충분하며 자동 임계 구역을 만들지 않는다. far pointer와 복합 read-modify-write는 명시적 `critical {}`이 필요하다. -- `interrupt fn`은 `interrupt_safe fn`만 호출할 수 있다. `interrupt_safe fn`은 `critical`, `@port_*`, `@volatile_*`, `asm`, 필요한 `unsafe`를 사용할 수 있다. 금지되는 것은 힙 할당, DOS/DPMI 서비스, 블로킹 I/O, 부동소수점 및 `interrupt_safe`가 아닌 함수 호출이다. 컴파일러가 본문만 보고 검증하고 이 효과를 `.fei` 시그니처에 기록한다. -- 전역에 대한 대여는 다음으로 제한한다. `static`(불변)은 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 대여할 수 없으며 직접 읽기와 쓰기만 허용한다. `shared var`는 `critical` 안에서의 직접 접근만 허용하고 대여할 수 없다. 전역 값을 참조로 넘겨야 하면 지역 변수로 복사한 뒤 대여한다. +**R10 (전역).** `static`은 불변이며 컴파일타임 상수 초기화만 가능하다. 일반 전역 `var`의 읽기와 쓰기는 안전하며 `unsafe`가 필요 없다. +- 전역에 대한 대여는 다음으로 제한한다. `static`(불변)은 `&`로 대여할 수 있다. 일반 전역 `var`는 `&`·`&mut` 모두 대여할 수 없으며 직접 읽기와 쓰기만 허용한다. 전역 값을 참조로 넘겨야 하면 지역 변수로 복사한 뒤 대여한다. - 이 제한의 근거는 R6다. 전역에 대한 대여가 살아 있는 동안 호출된 다른 함수가 같은 전역에 직접 접근할 수 있고, 그것은 함수 단위 지역 검사로 검출할 수 없다. 아래는 이 제한이 없으면 통과해 버리는 예다. ```fe @@ -323,7 +319,7 @@ fn f(r: &mut i32) { G = 5; } // r과 G가 같은 곳을 가리키는지 f는 fn g() { f(&mut G); } // 제한이 없으면 g의 지역 검사는 통과한다 ``` -- 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. `shared`, `atomic`, `critical`, `interrupt fn`은 v0.1에서 `bits16` 전용이며 `bits32`에서 사용하면 컴파일 에러다. +- 전역에는 `^T`나 `drop` 있는 타입을 둘 수 없다. 인터럽트 핸들러와 공유 상태(`shared`, `atomic`, `critical`, `interrupt fn`)는 v0.1에 없다(§11). **R11 (재귀·그래프 구조).** `^T`는 R4의 2급 참조가 아니므로 소유가 한 방향인 단방향 리스트와 트리는 필드에 저장할 수 있다. 반면 양방향 리스트·순환·일반 그래프는 역방향 필드에 `^T`를 두면 R1의 단일 소유권을 위반하고 `&T`를 두면 R4를 위반한다. 이런 구조는 아레나/배열이 값을 소유하고 `u16`/`u32` 인덱스 핸들이 간선을 나타내도록 구현한다. 표준 라이브러리 `mem.Arena`를 사용할 수 있으며, 핸들 역참조 때 세대 번호 또는 경계 검사를 사용해 해제된 항목 접근을 막아야 한다. @@ -342,7 +338,7 @@ decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl | const_decl | global_decl) | comptime_decl comptime_decl := 'comptime' 'if' expr '{' decl* '}' ['else' ('{' decl* '}' | comptime_decl)] -fn_decl := ['extern' string] [('interrupt' | 'interrupt_safe')] 'fn' ident +fn_decl := ['extern' string] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type generic_params := '(' ident (',' ident)* ')' @@ -356,7 +352,6 @@ error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}' const_decl := 'const' ident [':' type] '=' expr ';' global_decl := 'static' ident ':' type '=' expr ';' | 'var' ident ':' type '=' expr ';' - | 'shared' ['atomic'] 'var' ident ':' type '=' expr ';' block := '{' stmt* '}' stmt := 'let' ident [':' type] '=' expr ';' @@ -368,7 +363,6 @@ stmt := 'let' ident [':' type] '=' expr ';' | 'return' [expr] ';' | 'break' ';' | 'continue' ';' | 'defer' block | 'unsafe' block - | 'critical' block | 'comptime' 'if' expr block ['else' (block | 'if' ...)] | 'asm' '{' asm_body '}' | expr ';' @@ -390,8 +384,6 @@ qualified_name := ident ('.' ident)* type := qualified_name | '?' type | '!' type | qualified_name '!' type | '^' type | '&' ['mut'] type | '*' type - | 'far' ('^' | '*' | '&' ['mut']) type - | 'far' 'fn' '(' [type (',' type)*] ')' ['->' type] | '[' expr ']' type | '[' ']' ['mut'] type | 'fn' '(' [type (',' type)*] ')' ['->' type] | qualified_name '(' type (',' type)* ')' // 제네릭 인스턴스 @@ -439,9 +431,8 @@ orelse_expr := expr 'orelse' expr ``` @size_of(T) -> usize @align_of(T) -> usize -@bits -> comptime int @target -> comptime str +@target -> comptime str @ptr_cast(T, p) -> *T (unsafe) -@seg_ptr(T, seg: u16, off: u16) -> far *T (unsafe, bits16) @port_in8(p) @port_in16(p) @port_out8(p,v) @port_out16(p,v) (unsafe) @volatile_load(p) @volatile_store(p, v) (unsafe) @trap() -> never @unreachable() -> never (unsafe) @@ -451,8 +442,6 @@ orelse_expr := expr 'orelse' expr @fprint(w, fmt, ...) -> !void // 임의 Writer @sprint(buf: []mut u8, fmt, ...) -> usize // 버퍼에 기록, 쓴 바이트 수 반환 @compile_error(msg) // comptime에서 항상 컴파일 에러 -@as_far_fn(f) -> far fn() // bits16 전용 함수 포인터 변환 -@call_far(p: far fn()) // bits16/unsafe 전용 호출 ``` ### 6.3.1 포매팅 빌트인 @@ -489,28 +478,21 @@ io.write(out, " name="); io.write(out, s); io.write(out, "\n"); `@compile_error(msg)`의 `msg`는 comptime 문자열이어야 하며, 평가되는 분기에서 항상 진단을 발생시킨다. `comptime if`의 제거되는 분기에서는 진단하지 않는다. -`@as_far_fn(f)`와 `@call_far`는 `bits16`에서만 허용된다. 전자는 함수 포인터를 -`far fn()`으로 변환하고 후자는 `far fn()`을 호출한다. 둘 다 `unsafe { }` 안에서만 -사용할 수 있으며, `bits32`에서는 컴파일 에러다. ### 6.4 예제 ```fe -unit vga; -import std.sys; +unit frame; -const WIDTH: u16 = 320; -const HEIGHT: u16 = 200; +const WIDTH: usize = 320; +const HEIGHT: usize = 200; -pub fn set_mode13() { - unsafe { asm { mov ax, 0x0013; int 0x10; } } -} +pub struct Buffer { + pixels: []mut u8, -pub fn put_pixel(x: u16, y: u16, c: u8) { - if x >= WIDTH or y >= HEIGHT { return; } - unsafe { - let vram: far *u8 = @seg_ptr(u8, 0xA000, 0); - @volatile_store(vram + (y * WIDTH + x) as usize, c); + pub fn put(self: &mut Self, x: usize, y: usize, c: u8) -> void { + if x >= WIDTH or y >= HEIGHT { return; } + self.pixels[y * WIDTH + x] = c; } } ``` @@ -572,24 +554,22 @@ pub fn main() -> !void { ### 7.3 함수 호출 규약 -- 기본: `bits32`는 cdecl, `bits16`은 타깃 C 컴파일러 기본. +- 기본: cdecl. - `extern "c" fn name(...) -> T;` — 본문 없이 선언, C 심볼과 링크. 이름 맹글링 없음. 인자/반환에 `^T`, 슬라이스, 에러 유니온 사용 불가(`*T`, `usize`만). -- `interrupt fn name()` — 모든 레지스터 보존 + `iret`. 파라미터/반환 없음. 주소는 `@as_far_fn(name)`으로 획득. 호출 제한과 공유 상태 규칙은 R10을 따른다. -- `interrupt_safe fn name(...)` — 인터럽트 문맥에서 호출 가능한 함수. ABI는 일반 함수와 같고 R10의 제한을 본문 검사로 만족해야 한다. - 큰 struct(> 4바이트)는 숨은 포인터로 반환(C ABI 따름). ### 7.4 검사와 트랩 트랩 발생 조건: 배열/슬라이스 경계 초과, 정수 오버플로, 0 나눗셈, `?T`의 `.?` 실패, `@trap()`. -동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 등록된 `sys.on_exit(fn)` 정리 함수를 역순 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. 일반 panic unwind나 defer 실행은 없지만 bits16 interrupt vector처럼 프로세스 종료 전에 반드시 복원할 자원은 allocation 없는 고정 크기 `on_exit` registry에 등록한다. +동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 등록된 `sys.on_exit(fn)` 정리 함수를 역순 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. 일반 panic unwind나 defer 실행은 없지만 프로세스 종료 전에 반드시 복원해야 하는 자원은 allocation 없는 고정 크기 `on_exit` registry에 등록한다. `--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. **절대 제거되지 않는 것:** 소유권/참조 검사, 옵셔널 타입 검사, `match` 완전성 — 전부 컴파일타임이므로. ### 7.5 comptime -- `const` 선언의 초기값은 컴파일타임 평가(정수 연산, `@size_of`, `@bits`, 다른 const). +- `const` 선언의 초기값은 컴파일타임 평가(정수 연산, `@size_of`, 다른 const). - `comptime if`는 평가되지 않는 분기를 **파싱은 하되 타입 검사/코드 생성하지 않는다**(타깃별 분기용). - 함수의 `comptime` 파라미터는 §9 제네릭. - 재귀 평가 깊이 제한 256, 초과 시 에러. @@ -778,7 +758,8 @@ binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다 | 기능 | 등급 | 제외 이유 | 대체 수단 | |---|---|---|---| | 트레잇/인터페이스 (`dyn`) | **v0.2 (1순위)** | 부트스트랩에 불필요, 타입 시스템 전반에 영향 | Copy handle enum (`io.Writer`, §10) | -| bits32 interrupt/shared/critical | v0.2 | DPMI callback·vector 복원과 backend 지원이 아직 범위 밖 | bits16 실행 파일 또는 polling | +| 인터럽트 핸들러와 공유 상태 (`interrupt fn`, `shared`, `atomic`, `critical`) | v0.2 | 벡터 설치·복원과 배리어가 백엔드 지원을 요구하고, 타깃마다 다르다 | polling | +| far 포인터와 세그먼트 주소 지정 | **영구** | x86 리얼모드에만 있는 개념이고 평평한 주소 공간에는 대응물이 없다 (§2) | 없음. 리얼모드 타깃이 생기면 그때 다시 본다 | | 클로저 | v0.2 | 캡처 = 참조 저장 = R4 위반 소지 | 콜백에 `ctx: *void` 전달 | | 연산자 오버로딩 | v0.2 (인터페이스 이후) | 숨은 비용. 넣더라도 특정 인터페이스 구현으로만 제한 | 메서드 | | 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | diff --git a/fec/src/driver.c b/fec/src/driver.c index dddcd72..91dc7cf 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -16,7 +16,7 @@ static char *read_file(const char *name, unsigned long *size) static void usage(void) { - puts("usage: fec [--dump-tokens|--dump-ast|--check|--emit-c] file.fe [--target=bits16|bits32] [-o output.c]"); + puts("usage: fec [--dump-tokens|--dump-ast|--check] file.fe [--no-checks]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -45,14 +45,12 @@ int main(int argc, char **argv) FeAst ast; FeParser p; FeCheck check; - unsigned pointer_bits=32; + unsigned pointer_bits=FE_PTR_BITS; if(argc<2){usage();return 2;} for(i=1;itext=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=type(p); return n; } - if (is(p,FE_TOK_FAR)) { - next(p); n=toknode(p,FE_N_TYPE,t); if(is(p,FE_TOK_STAR)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)) next(p); n->a=type(p); return n; - } if (is(p,FE_TOK_LBRACKET)) { next(p); n=toknode(p,FE_N_TYPE,t); if(!eat(p,FE_TOK_RBRACKET)) { n->a=expr(p,0); want(p,FE_TOK_RBRACKET,"expected ']' in array type"); } diff --git a/fec/src/types.c b/fec/src/types.c index 2ec7b77..1762bc9 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -441,12 +441,12 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) if (t->kind == FE_TYPE_ERROR_UNION) { if (t->error_value && t->error_value->kind != FE_TYPE_VOID) { layout_type(ctx,t->error_value); - t->align=ctx->pointer_bits==16 ? 1U : fe_type_align(t->error_value); + t->align=fe_type_align(t->error_value); t->size=round_up(2UL,t->align)+fe_type_size(t->error_value); t->size=round_up(t->size,t->align); } else { t->size=2; - t->align=ctx->pointer_bits==16 ? 1U : 2U; + t->align=2U; } t->cycle_state = 2; return; } @@ -456,7 +456,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) t->size=fe_type_size(t->elem); t->align=fe_type_align(t->elem); } else { - t->align=ctx->pointer_bits==16 ? 1U : fe_type_align(t->elem); + t->align=fe_type_align(t->elem); t->size=round_up(1UL,t->align)+fe_type_size(t->elem); t->size=round_up(t->size,t->align); } @@ -467,30 +467,29 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) } if (t->kind == FE_TYPE_INT) { t->size = (t->bits + 7U) / 8U; - t->align = ctx->pointer_bits == 16 ? 1U : t->size; - if (t->size > 4UL) t->size = ctx->pointer_bits == 16 ? 2UL : 4UL; + t->align = t->size; + if (t->size > 4UL) t->size = 4UL; t->cycle_state = 2; return; } if (t->kind == FE_TYPE_REF) { - t->size = ctx->pointer_bits == 16 ? 2UL : 4UL; - t->align = ctx->pointer_bits == 16 ? 1U : 4U; + t->size = FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; t->cycle_state = 2; return; } if (t->kind == FE_TYPE_OWNED) { t->size = t->elem && t->elem->kind==FE_TYPE_SLICE ? - (ctx->pointer_bits == 16 ? 4UL : 8UL) : - (ctx->pointer_bits == 16 ? 2UL : 4UL); - t->align = ctx->pointer_bits == 16 ? 1U : 4U; + 2UL * FE_PTR_SIZE : FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; t->cycle_state = 2; return; } if (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR) { - t->size = ctx->pointer_bits == 16 ? 4UL : 8UL; - t->align = ctx->pointer_bits == 16 ? 1U : 4U; + t->size = 2UL * FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; t->cycle_state = 2; return; } if (t->kind == FE_TYPE_ARRAY) { layout_type(ctx, t->elem); - t->align = t->packed || ctx->pointer_bits == 16 ? 1U : fe_type_align(t->elem); + t->align = t->packed ? 1U : fe_type_align(t->elem); t->size = t->length * fe_type_size(t->elem); t->cycle_state = 2; return; } @@ -500,7 +499,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) if (!t->fields[i].type && t->fields[i].ast_node) t->fields[i].type = fe_type_from_ast(ctx, t->fields[i].ast_node->a); layout_type(ctx, t->fields[i].type); - align = t->packed || ctx->pointer_bits == 16 ? 1U : fe_type_align(t->fields[i].type); + align = t->packed ? 1U : fe_type_align(t->fields[i].type); if (align > max_align) max_align = align; off = round_up(off, align); t->fields[i].offset = off; @@ -528,9 +527,9 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) if (off > max_size) max_size = off; } t->bits = t->variant_count > 256U ? 16U : 8U; - off = ctx->pointer_bits == 16 ? t->bits / 8U : round_up(t->bits / 8U, max_align); - t->size = round_up(off + max_size, ctx->pointer_bits == 16 ? 1U : max_align); - t->align = ctx->pointer_bits == 16 ? 1U : max_align; + off = round_up(t->bits / 8U, max_align); + t->size = round_up(off + max_size, max_align); + t->align = max_align; t->cycle_state = 2; } } @@ -599,8 +598,7 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) fe_type_from_ast(ctx,node->b)); return fe_type_error_union(ctx,fe_type_from_ast(ctx,node->a)); } - if (node->text && (strcmp(node->text, "*") == 0 || - strcmp(node->text, "far") == 0)) + if (node->text && strcmp(node->text, "*") == 0) return fe_type_intern(ctx, ""); if (node->text && strcmp(node->text, "fn") == 0) return fe_type_intern(ctx, ""); @@ -665,8 +663,8 @@ const char *fe_type_c_name(const FeType *t, unsigned pointer_bits) return owned_name; } if (t->kind != FE_TYPE_INT) return "long"; - if (strcmp(t->name, "usize") == 0) return pointer_bits == 16 ? "unsigned short" : "unsigned long"; - if (strcmp(t->name, "isize") == 0) return pointer_bits == 16 ? "short" : "long"; + if (strcmp(t->name, "usize") == 0) return "unsigned long"; + if (strcmp(t->name, "isize") == 0) return "long"; if (strcmp(t->name, "i8") == 0) return "signed char"; if (strcmp(t->name, "u8") == 0) return "unsigned char"; if (strcmp(t->name, "i16") == 0) return "short"; diff --git a/fec/src/types.h b/fec/src/types.h index 4afabe4..5ade57c 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -10,6 +10,13 @@ typedef enum FeTypeKind { FE_TYPE_REF, FE_TYPE_OWNED, FE_TYPE_UNKNOWN } FeTypeKind; +/* One target, one pointer width (SPEC 2). usize and isize are that width and + are not promised to be any particular number of bits, which is what keeps a + different width possible later. */ +#define FE_PTR_SIZE 4UL +#define FE_PTR_ALIGN 4U +#define FE_PTR_BITS 32U + typedef struct FeFieldType FeFieldType; /* A type parameter bound to an argument while an instance is checked. */ diff --git a/fec/tests/parse/v012form.fe b/fec/tests/parse/v012form.fe index 406a63d..6175e55 100644 --- a/fec/tests/parse/v012form.fe +++ b/fec/tests/parse/v012form.fe @@ -7,7 +7,6 @@ packed struct Packet { } interrupt_safe fn poll() { } interrupt fn timer() { } -fn invoke(p: far fn()) { } pub fn demo() { var count = undefined; @@ -16,5 +15,4 @@ pub fn demo() { let x = true and not false or false; let y = x orelse true; let e = error.NotFound; - @call_far(@as_far_fn(timer)); } From b0e55e072ed1d384f2b686bb9bb82819dbd38156 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:41:10 +0900 Subject: [PATCH 134/184] =?UTF-8?q?spec:=20FE=5FTOK=5FFAR=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fec/src/lexer.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fec/src/lexer.h b/fec/src/lexer.h index 7b1cda7..5c3bb47 100644 --- a/fec/src/lexer.h +++ b/fec/src/lexer.h @@ -11,7 +11,7 @@ typedef enum FeTokKind { FE_TOK_IF, FE_TOK_ELSE, FE_TOK_WHILE, FE_TOK_FOR, FE_TOK_IN, FE_TOK_MATCH, FE_TOK_RETURN, FE_TOK_BREAK, FE_TOK_CONTINUE, FE_TOK_DEFER, FE_TOK_UNSAFE, FE_TOK_COMPTIME, FE_TOK_ASM, FE_TOK_TRY, FE_TOK_CATCH, FE_TOK_AS, FE_TOK_EXTERN, - FE_TOK_INTERRUPT, FE_TOK_INTERRUPT_SAFE, FE_TOK_FAR, FE_TOK_TRUE, FE_TOK_FALSE, FE_TOK_NULL, + FE_TOK_INTERRUPT, FE_TOK_INTERRUPT_SAFE, FE_TOK_TRUE, FE_TOK_FALSE, FE_TOK_NULL, FE_TOK_UNDEFINED, FE_TOK_SHARED, FE_TOK_ATOMIC, FE_TOK_CRITICAL, FE_TOK_SELF, FE_TOK_SELFTYPE, FE_TOK_TYPE, FE_TOK_PACKED, FE_TOK_ORELSE, FE_TOK_LPAREN, FE_TOK_RPAREN, FE_TOK_LBRACE, FE_TOK_RBRACE, FE_TOK_LBRACKET, FE_TOK_RBRACKET, From ddea962d144bfdd9821d50fdb7bbccff5b751f24 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:44:56 +0900 Subject: [PATCH 135/184] =?UTF-8?q?ir:=20=EC=A4=91=EA=B0=84=20=ED=91=9C?= =?UTF-8?q?=ED=98=84=EC=9D=84=20=EC=A0=95=EC=9D=98=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 명령 12개와 종결자 4개. 함수 단위 기본 블록이고 임시값은 블록을 넘지 않아 phi 노드가 없다 -- 블록을 넘겨야 하는 값은 지역을 경유한다. 코드가 조금 더 생기지만 레지스터 할당기를 블록 단위로 유지해 준다. Ferro 타입은 여기서 사라진다. 구조체·슬라이스·옵셔널·에러 유니온이 전부 mem 이고 필드는 lowering 이 계산한 바이트 오프셋이다. 모노모피제이션이 프론트엔드에서 끝나므로 IR 에 제네릭이라는 개념도 없다. 덩어리는 언제나 주소로 오간다. 크기 임계값이 없어서 ISA 마다 다른 구조체 전달 규칙을 통째로 피해간다. trap 은 이유와 줄 번호만 남기고 파일 이름 문자열은 유닛당 하나를 공유한다. IR.md 가 설명이고 ir.h/ir.c 가 그 형태다. 아직 아무도 만들지 않는다. --- IR.md | 180 +++++++++++++++++++++++ TODO.md | 111 +++++--------- fec/src/ir.c | 408 +++++++++++++++++++++++++++++++++++++++++++++++++++ fec/src/ir.h | 177 ++++++++++++++++++++++ tests/run.py | 2 +- 5 files changed, 805 insertions(+), 73 deletions(-) create mode 100644 IR.md create mode 100644 fec/src/ir.c create mode 100644 fec/src/ir.h diff --git a/IR.md b/IR.md new file mode 100644 index 0000000..856e1be --- /dev/null +++ b/IR.md @@ -0,0 +1,180 @@ +# Ferro IR + +`fec` 의 중간 표현. 이 문서는 IR 자체만 다룬다. 언어 규범은 `SPEC.md` 이고, +Ferro 의미론이 어떻게 이 형태로 펴지는지는 lowering 이 담당한다. + +IR 을 두는 이유는 하나다. **검사가 끝난 AST 와 기계 사이의 거리가 너무 멀다.** +`try` 하나가 분기 두 개와 임시값 하나로 펴지고, `defer` 는 함수의 모든 이탈 +경로에 복제되며, 배열 인덱스는 비교와 트랩을 낳는다. 그 전개를 명령어 선택과 +같은 자리에서 하면 둘 다 읽을 수 없게 된다. + +--- + +## 1. 형태 + +함수 단위다. 함수는 **기본 블록**의 목록이고, 블록은 **명령**의 목록과 하나의 +**종결자**로 끝난다. 블록 중간에서 분기하지 않고, 종결자 뒤에 명령이 없다. + +``` +fn @okid.test() -> i32 { + $0: i32 ; 지역 + b0: + %0 = const i32 7 + store $0, %0 + %1 = load i32 $0 + ret %1 +} +``` + +### 슬롯 + +| | | | +|---|---|---| +| `%n` | 임시값 | 한 번 정의되고 여러 번 쓰인다. 블록을 넘지 않는다 | +| `$n` | 지역 | 주소를 가진 스택 자리. 함수 진입 시 전부 잡는다 | +| `@name` | 전역·함수 | 링커가 보는 이름 | + +임시값이 블록을 넘지 않으므로 φ 노드가 없다. 블록을 넘겨야 하는 값은 지역에 +`store` 하고 다시 `load` 한다. 이것이 SSA 보다 코드를 조금 더 만들지만, +**레지스터 할당기를 블록 단위로 유지**해 준다. v0.1 에서는 그 교환이 맞다. + +### 기계 타입 + +``` +i8 i16 i32 ptr mem +``` + +Ferro 타입은 여기서 사라진다. 구조체·배열·슬라이스·옵셔널·에러 유니온은 전부 +`mem` 이고, 필드 접근은 lowering 이 계산한 **바이트 오프셋**이다. IR 은 +`Box(i32)` 라는 것을 모른다 — 크기 N 바이트짜리 메모리만 안다. + +`ptr` 은 4바이트다. 세그먼트가 없으므로 포인터 종류도 하나뿐이다 (`SPEC.md` §2). + +`bool` 은 `i8`, `char` 는 `i8`, `usize` 는 `i32` 다. + +--- + +## 2. 명령 + +12개다. + +``` +%d = const 상수 +%d = load 메모리에서 읽는다 + store , %v 메모리에 쓴다 +%d = addr 주소를 뜬다 +%d = %a, %b 산술·비트 연산 +%d = %a, %b 비교. 결과는 i8 +%d = cast %a 정수 폭 변환 +%d = call @f(%a, ...) 호출 + copy , , N N바이트 복사 +``` + +`` — `add sub mul div mod and or xor shl shr` +`` — `eq ne lt le gt ge` (부호 있음/없음은 `` 가 정한다) + +### place + +`load`·`store`·`addr`·`copy` 의 피연산자다. + +``` +$n 지역 +@name 전역 +%p 포인터 임시값이 가리키는 곳 +%p + 상수 오프셋. 필드 접근이 여기로 온다 +``` + +인덱스처럼 오프셋이 상수가 아니면 lowering 이 주소를 먼저 계산한다. + +``` +; a[i] 는 +%0 = load i32 $i +%1 = const i32 4 ; 원소 크기 +%2 = mul i32 %0, %1 +%3 = addr $a +%4 = add ptr %3, %2 +%5 = load i32 %4 +``` + +### 덩어리 + +**덩어리는 언제나 주소로 오간다.** 크기 임계값이 없다. + +``` +call @f(%p) ; f 가 mem 을 받으면 %p 는 그 주소다 +``` + +반환도 같다. `mem` 을 반환하는 함수는 **첫 인자로 결과를 쓸 주소를 받는다**. +호출자가 자리를 잡고 넘긴다. + +이 규약을 고른 이유는 단순해서만이 아니다. ISA 마다 다른 "구조체를 언제 +레지스터로 넘기는가" 규칙을 통째로 피해간다. `extern "c"` 경계에서는 C ABI 로 +변환해야 하며 그것은 백엔드의 일이다. + +--- + +## 3. 종결자 + +4개다. + +``` +jmp b 무조건 분기 +br %c, b, b %c 가 0이 아니면 b +ret [%v] 반환 +trap 중단 +``` + +`trap` 은 `fe_trap(reason, UNIT_FILE, line)` 이 된다. `reason` 은 작은 정수이고 +`UNIT_FILE` 은 **유닛당 하나뿐인 파일 이름 문자열**이다. 트랩 지점마다 문자열을 +두면 실행 파일이 부풀기 때문이다. + +`line` 은 컴파일 시점에 상수로 박힌다. 주소가 아니라 줄 번호를 남기는 방식은 +당대에 흔했고, 심볼 테이블 없이 실패 지점을 말할 수 있는 가장 싼 방법이다. + +reason 값: + +| | | +|---|---| +| 0 | 배열·슬라이스 경계 | +| 1 | 정수 오버플로 | +| 2 | 0으로 나눔 | +| 3 | 도달할 수 없는 곳에 도달 (`@unreachable`) | +| 4 | 명시적 `@trap()` | + +`--no-checks` 는 0·1·2 를 만드는 검사를 lowering 단계에서 생략한다. 3·4 는 +소스에 쓰인 것이므로 남는다. + +--- + +## 4. 함수와 전역 + +``` +fn @unit.name(, ...) -> { ... } +extern fn @name(, ...) -> +global @unit.name : mem = <초기값 바이트> +``` + +이름은 `유닛.이름` 이다. 제네릭 인스턴스도 여기서는 그냥 함수 하나다 — +모노모피제이션이 프론트엔드에서 끝나므로 **IR 에 제네릭이라는 개념이 없다.** + +--- + +## 5. 이 IR 이 하지 않는 것 + +- **최적화 없음.** 상수 접기도, 죽은 코드 제거도 없다. 나중에 붙일 자리는 있다 +- **φ 노드 없음.** 블록 간 값은 지역을 경유한다 +- **타입 검사 없음.** 검사는 프론트엔드에서 끝났다. IR 이 잘못되었다면 lowering 의 버그다 +- **예외·언와인딩 없음.** 에러는 값이고, `try` 는 분기다 + +--- + +## 6. 왜 이 크기인가 + +명령 12개와 종결자 4개는 **i386 으로 직접 번역할 수 있는 최소 집합**이다. +각각이 몇 개의 x86 명령으로 내려가고, 그 대응이 눈으로 확인된다. 이보다 높으면 +백엔드가 IR 을 다시 해석해야 하고, 이보다 낮으면 lowering 이 기계에 가까워져서 +다른 ISA 로 옮길 때 다시 써야 한다. + +같은 이유로 이 집합은 m68k·ARM·MIPS·RV32 에도 그대로 내려간다. 평평한 주소 +공간과 정수 연산만 쓰기 때문이다. 백엔드를 하나 더 만드는 비용은 명령 선택과 +레지스터 할당이지 IR 재설계가 아니다. diff --git a/TODO.md b/TODO.md index 828d202..0d5f8d3 100644 --- a/TODO.md +++ b/TODO.md @@ -1,38 +1,31 @@ # TODO -현재: **150/188** (`uv run python tests/run.py`) +현재: **188/188** (`uv run python tests/run.py`) -컴파일러는 프론트엔드뿐이다. IR·lowering·백엔드는 없다. +프론트엔드는 끝났다. 목표는 **i386 백엔드 + stdlib 으로 Windows 11 용 컴파일러를 +완성하는 것**이다. 아직 실행된 Ferro 프로그램은 하나도 없다. --- -## 프론트엔드 마무리 - -| # | 일 | 규모 | fixture | 비고 | -|---|---|---|---|---| -| 1 | cross-unit 이름 해석 | 중 | units 8 | `util.answer()` 가 다른 유닛 선언을 보게. `check.c` 가 import binding 을 심볼로 알아야 함 | -| 2 | 가시성 `pub`/private | 중 | units 4 | `privfn` `privfld` `dotpriv` `pubpriv` | -| 3 | nominal error identity | 중 | units 3 | `errsame` `errdet` `errnom` | -| 4 | **제네릭 실체화** | 대 | generic 25 | worklist 엔진. **IR 설계에 직접 영향 — 분기점** | -| 5 | 마커 없는 fixture 37개 판정 | 중 | — | 개당 판단 필요. 아래 참조 | - -## 백엔드 +## 남은 일 | # | 일 | 규모 | 비고 | |---|---|---|---| -| 6 | IR 정의 | 중 | 초안 있음. 4번이 정해져야 확정 | -| 7 | **lowering** | 대 | `try`/`catch`/`defer`/drop/`for`/메서드/경계검사 전개. **프로젝트 무게중심** | -| 8 | i386 백엔드 | 대 | IR → x86 asm → `wasm` → `wlink`. W11 에서 실행 검증 | -| 9 | 런타임 최소 세트 | 소~중 | 시작 스텁, `fe_trap`, `malloc`/`free` | -| 10 | `pending-backend/` 3개 복귀 | 소 | bounds trap, `--no-checks` 차등, 소유권 drop | +| 1 | **IR 정의** | 중 | 3-address, 기본 블록, 함수 단위. 명령 12 + 종결자 4 | +| 2 | **lowering** | 대 | `try`/`catch`/`orelse`/`defer`/drop/`for`/메서드/경계검사/옵셔널·에러유니온 구성/제네릭 인스턴스 전개. **프로젝트 무게중심** | +| 3 | i386 백엔드 | 대 | IR → x86 asm → `wasm` → `wlink` → PE. 명령 선택, 레지스터 할당, 호출 규약 | +| 4 | 런타임 | 소~중 | 시작 스텁, `fe_trap(reason, file, line)`, 할당, 종료 | +| 5 | `pending-backend/` 4개 복귀 | 소 | bounds trap, `--no-checks` 차등, 소유권 drop. **처음으로 실행이 검증됨** | +| 6 | stdlib 명세 | 중 | SPEC §10 이 플레이스홀더. 시그니처·오류·경계 동작 미정 | +| 7 | stdlib 구현 | 대 | `core` `mem` `fmt` `io` `sys`, 그리고 제네릭이 생겼으니 `list` `map` | ## 그 이후 -| # | 일 | 규모 | 비고 | -|---|---|---|---| -| 11 | stdlib | 대 | 현재 49줄, 본문 있는 함수 2개. `List`/`Map` 은 4번에 의존 | -| 12 | bits16 백엔드 | 대 | `DX:AX` 32비트 산술, 세그먼트. 제일 어려움. 마지막 | -| 13 | 타깃 의존 구문 | 중 | `far` `asm` `interrupt` `atomic` `critical` `shared` — 파싱만 되고 의미 없음. fixture 도 없음 | +| | | | +|---|---|---| +| 셀프호스팅 | 대 | `fec` 을 Ferro 로. 여기까지 오면 언어가 자기 무게를 견딘다는 증거 | +| 인터럽트·공유 상태 | 중 | `interrupt` `shared` `atomic` `critical` — 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | +| 다른 32비트 타깃 | 대 | m68k / ARM / MIPS / RV32. IR 결정은 전부 ISA 중립이라 백엔드만 붙이면 됨 | --- @@ -40,27 +33,32 @@ | | 내용 | 언제 | |---|---|---| -| 덩어리 전달 규약 | 정함 (전부 주소로). 8번 착수 전 재확인 | 8 전 | -| `extern` C 상호운용 | 위 규약이 C ABI 와 안 맞음. 경계 변환 필요 | 8 전 | -| 주 타깃 | i386/W11 로 기울었으나 SPEC §1.2 는 아직 "640KB 셀프호스팅" | 아무 때나 | -| stdlib 명세 | SPEC §10 이 플레이스홀더. 시그니처·오류·경계 동작 미정 | 11 전 | -| `far` 의 거취 | IR 에 안 두기로 함. 언어에서 뺄지 bits16 전용으로 둘지는 미정 | 12 전 | +| 덩어리 전달 규약 | 정함 (전부 주소로). 3번 착수 전 재확인 | 3 전 | +| `extern` C 상호운용 | 위 규약이 C ABI 와 안 맞음. 경계 변환 필요 | 3 전 | +| 엔디안 | 리틀엔디안 가정. `packed struct` 가 바이트 배치를 약속하므로, 빅엔디안 타깃(m68k·MIPS·POWER)이 생기면 타깃 파라미터가 된다 | 다른 타깃 전 | +| stdlib 명세 | SPEC §10 이 플레이스홀더 | 6 | + +## 정해진 것 + +| | | +|---|---| +| 타깃 | **i386 하나.** 세그먼트 없음, `far` 영구 제외 (SPEC §2) | +| `usize`/`isize` | **타깃의 포인터 폭.** 비트 수를 약속하지 않으므로 64비트 문이 닫히지 않음 | +| 제네릭 | 모노모피제이션. 순수 프론트엔드 기능이라 IR 에 제네릭 개념이 없음 | +| 덩어리 전달 | 전부 주소로. 크기 임계값 없음 — ISA 마다 다른 구조체 전달 ABI 를 피해감 | +| 트랩 | `trap ` → `fe_trap(reason, UNIT_FILE, line)`. 유닛당 파일 문자열 하나 | +| 셀프호스팅 | 640KB 목표 아님. 32비트 보호모드에서 돈다 (SPEC §2.1) | --- ## 순서 ``` -1 → 2 → 3 프론트엔드 유닛 완성 -4 제네릭. ← 여기서 IR 형태가 결정됨 -6 → 7 IR + lowering -8 → 9 → 10 i386 백엔드, 실행 검증 복귀 -11 stdlib -12 → 13 DOS 고유 +1 → 2 IR + lowering +3 → 4 → 5 i386 백엔드, 런타임, 실행 검증 복귀 ← 여기서 처음 실행됨 +6 → 7 stdlib ``` -5번은 독립적이라 아무 때나 끼워 넣는다. - --- ## 위임 정책 @@ -73,43 +71,12 @@ **위임 불가** — 언어 의미론 판단이 섞인 것 -- 마커 판정, cross-unit 해석, 가시성, 제네릭, IR/lowering/백엔드 +- 마커 판정, IR/lowering/백엔드, stdlib 명세 --- -## 5번에 대하여 (마커 없는 fixture 37개) +## 미뤄둔 정리 작업 -거부 fixture 76개 중 **37개에 `// ERROR:` 마커가 없다.** 마커가 없으면 러너는 -"거부되기만 하면 통과" 로 판정한다. 즉 **엉뚱한 이유로 거부돼도 초록이다.** - -``` -format 10 own 8 types 19 -``` - -마커를 붙이려면 fixture 마다 "지금 나오는 진단이 옳은가" 를 판정해야 하고, -그 판정은 세 갈래로 갈린다. 오늘 5건을 조사했을 때 실제로 셋 다 나왔다: - -| 결론 | 예 | 조치 | -|---|---|---| -| 마커가 틀렸다 | `badbrmov` 한 줄 밀림 | 마커 수정 | -| 마커가 언어를 오해했다 | `badloop` — 루프 안에서 이미 걸림 | 마커 수정 | -| **진단이 부실하다** | `badweak` — "type mismatch" 로만 나옴 | **컴파일러 수정** | - -세 번째 때문에 위임이 위험하다. 실제 출력을 마커에 그대로 베끼면 전부 초록이 -되지만 **컴파일러가 틀린 곳까지 정답으로 굳는다.** - -### 병렬화 - -판정 자체는 **fixture 간 독립**이라 병렬 가능하다. 다만 **판정을 위임하면 안 된다.** -쓸 수 있는 형태는 증거 수집과 판정을 나누는 것이다: - -``` -위임: fixture 를 열고 → fec 를 돌리고 → 다음을 보고 - 파일 경로 / 검사하려는 것으로 보이는 규칙 / 실제 진단 전문(줄·문구) - 마커는 만들지 않는다 - -직접: 보고를 읽고 세 갈래 중 무엇인지 판정 → 마커를 쓰거나 컴파일러를 고침 -``` - -이러면 37개를 디렉터리별로 병렬로 뿌려 증거를 모으고, 판정만 직접 하면 된다. -느린 부분(파일 읽기·실행·정리)이 병렬화되고 위험한 부분은 남는다. +fixture 122개 이름이 아직 DOS 8.3 시절 잔재라 무엇을 검사하는지 이름만으로는 +알 수 없다. 마커가 전부 붙었으므로 `bad`/`ok` 접두사는 이제 기대값이 아니고, +자유롭게 이름을 지을 수 있다. 급하지 않다. diff --git a/fec/src/ir.c b/fec/src/ir.c new file mode 100644 index 0000000..bff4452 --- /dev/null +++ b/fec/src/ir.c @@ -0,0 +1,408 @@ +#include "ir.h" +#include + +void fe_ir_module_init(FeIrModule *m) +{ + fe_arena_init(&m->arena, 16384); + m->unit_file = ""; + m->funcs = 0; + m->last_func = 0; + m->globals = 0; + m->last_global = 0; +} + +void fe_ir_module_destroy(FeIrModule *m) +{ + fe_arena_destroy(&m->arena); + m->funcs = 0; + m->last_func = 0; + m->globals = 0; + m->last_global = 0; +} + +static void *ir_alloc(FeIrModule *m, unsigned long size) +{ + return fe_arena_alloc(&m->arena, (size_t)size); +} + +FeIrFunc *fe_ir_func(FeIrModule *m, const char *name, FeIrType ret, + unsigned long ret_size) +{ + FeIrFunc *f = (FeIrFunc *)ir_alloc(m, sizeof(FeIrFunc)); + if (!f) return 0; + memset(f, 0, sizeof *f); + f->name = name; + f->ret = ret; + f->ret_size = ret_size; + /* An aggregate result is written through a hidden first parameter, so the + caller owns the storage and no size threshold has to be agreed on. */ + f->returns_by_address = ret == FE_IR_MEM; + if (m->last_func) m->last_func->next = f; + else m->funcs = f; + m->last_func = f; + return f; +} + +unsigned fe_ir_local(FeIrModule *m, FeIrFunc *f, FeIrType type, + unsigned long size, unsigned align, const char *name) +{ + if (f->local_count == f->local_capacity) { + unsigned cap = f->local_capacity ? f->local_capacity * 2U : 8U; + FeIrLocal *grown = (FeIrLocal *)ir_alloc(m, cap * sizeof(FeIrLocal)); + if (!grown) return 0; + if (f->locals) + memcpy(grown, f->locals, f->local_count * sizeof(FeIrLocal)); + f->locals = grown; + f->local_capacity = cap; + } + f->locals[f->local_count].type = type; + f->locals[f->local_count].size = size; + f->locals[f->local_count].align = align ? align : 1U; + f->locals[f->local_count].name = name; + return f->local_count++; +} + +unsigned fe_ir_temp(FeIrFunc *f) +{ + return f->temp_count++; +} + +FeIrBlock *fe_ir_block(FeIrModule *m, FeIrFunc *f) +{ + FeIrBlock *b = (FeIrBlock *)ir_alloc(m, sizeof(FeIrBlock)); + if (!b) return 0; + memset(b, 0, sizeof *b); + b->id = f->block_count++; + b->func = f; + /* Until something says otherwise a block falls off the end, which is only + correct for a void function; lowering always sets a real terminator. */ + b->term = FE_IR_RET; + if (f->last) f->last->next = b; + else f->first = b; + f->last = b; + return b; +} + +FeIrPlace fe_ir_at_local(unsigned index, long offset) +{ + FeIrPlace p; + p.base = FE_PLACE_LOCAL; p.index = index; p.name = 0; p.offset = offset; + return p; +} + +FeIrPlace fe_ir_at_global(const char *name, long offset) +{ + FeIrPlace p; + p.base = FE_PLACE_GLOBAL; p.index = 0; p.name = name; p.offset = offset; + return p; +} + +FeIrPlace fe_ir_at_temp(unsigned temp, long offset) +{ + FeIrPlace p; + p.base = FE_PLACE_TEMP; p.index = temp; p.name = 0; p.offset = offset; + return p; +} + +static FeIrValue *emit(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t) +{ + FeIrValue *v = (FeIrValue *)ir_alloc(m, sizeof(FeIrValue)); + if (!v) return 0; + memset(v, 0, sizeof *v); + v->op = op; + v->type = t; + if (b->last) b->last->next = v; + else b->first = v; + b->last = v; + return v; +} + +/* A result needs a fresh temporary, and the counter lives on the function, so + a block carries the function it is being built in. */ +static unsigned result(FeIrModule *m, FeIrBlock *b, FeIrValue *v) +{ + (void)m; + v->has_dest = 1; + v->dest = fe_ir_temp(b->func); + return v->dest; +} + +unsigned fe_ir_const(FeIrModule *m, FeIrBlock *b, FeIrType t, long value) +{ + FeIrValue *v = emit(m, b, FE_IR_CONST, t); + if (!v) return 0; + v->imm = value; + return result(m, b, v); +} + +unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p) +{ + FeIrValue *v = emit(m, b, FE_IR_LOAD, t); + if (!v) return 0; + v->place = p; + return result(m, b, v); +} + +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned value) +{ + FeIrValue *v = emit(m, b, FE_IR_STORE, FE_IR_VOID); + if (!v) return; + v->place = p; + v->a = value; +} + +unsigned fe_ir_addr(FeIrModule *m, FeIrBlock *b, FeIrPlace p) +{ + FeIrValue *v = emit(m, b, FE_IR_ADDR, FE_IR_PTR); + if (!v) return 0; + v->place = p; + return result(m, b, v); +} + +unsigned fe_ir_binary(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t, + unsigned a, unsigned c, int is_unsigned) +{ + FeIrValue *v; + int is_cmp = op >= FE_IR_EQ && op <= FE_IR_GE; + v = emit(m, b, op, is_cmp ? FE_IR_I8 : t); + if (!v) return 0; + v->a = a; + v->b = c; + v->is_unsigned = is_unsigned; + /* A comparison reports i8 but reads its operands at `t`, so the width has + to survive somewhere the backend can see it. */ + if (is_cmp) v->imm = (long)t; + return result(m, b, v); +} + +unsigned fe_ir_cast(FeIrModule *m, FeIrBlock *b, FeIrType from, FeIrType to, + unsigned a, int is_unsigned) +{ + FeIrValue *v = emit(m, b, FE_IR_CAST, to); + if (!v) return 0; + v->a = a; + v->imm = (long)from; + v->is_unsigned = is_unsigned; + return result(m, b, v); +} + +unsigned fe_ir_call(FeIrModule *m, FeIrBlock *b, FeIrType ret, + const char *callee, unsigned *args, unsigned count) +{ + FeIrValue *v = emit(m, b, FE_IR_CALL, ret); + unsigned i; + if (!v) return 0; + v->callee = callee; + v->arg_count = count; + if (count) { + v->args = (unsigned *)ir_alloc(m, count * sizeof(unsigned)); + if (v->args) for (i = 0; i < count; ++i) v->args[i] = args[i]; + else v->arg_count = 0; + } + if (ret == FE_IR_VOID) return 0; + return result(m, b, v); +} + +void fe_ir_copy(FeIrModule *m, FeIrBlock *b, FeIrPlace dst, FeIrPlace src, + unsigned long size) +{ + FeIrValue *v = emit(m, b, FE_IR_COPY, FE_IR_VOID); + if (!v) return; + v->place = dst; + v->place2 = src; + v->imm = (long)size; +} + +void fe_ir_jmp(FeIrBlock *b, unsigned target) +{ + b->term = FE_IR_JMP; + b->target = target; +} + +void fe_ir_br(FeIrBlock *b, unsigned cond, unsigned t, unsigned f) +{ + b->term = FE_IR_BR; + b->cond = cond; + b->target = t; + b->target_else = f; +} + +void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value) +{ + b->term = FE_IR_RET; + b->ret_value = value; + b->has_ret_value = has_value; +} + +void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line) +{ + b->term = FE_IR_TRAP; + b->trap = reason; + b->trap_line = line; +} + +const char *fe_ir_type_name(FeIrType t) +{ + switch (t) { + case FE_IR_VOID: return "void"; + case FE_IR_I8: return "i8"; + case FE_IR_I16: return "i16"; + case FE_IR_I32: return "i32"; + case FE_IR_PTR: return "ptr"; + case FE_IR_MEM: return "mem"; + } + return "?"; +} + +const char *fe_ir_op_name(FeIrOp op) +{ + switch (op) { + case FE_IR_CONST: return "const"; + case FE_IR_LOAD: return "load"; + case FE_IR_STORE: return "store"; + case FE_IR_ADDR: return "addr"; + case FE_IR_ADD: return "add"; + case FE_IR_SUB: return "sub"; + case FE_IR_MUL: return "mul"; + case FE_IR_DIV: return "div"; + case FE_IR_MOD: return "mod"; + case FE_IR_AND: return "and"; + case FE_IR_OR: return "or"; + case FE_IR_XOR: return "xor"; + case FE_IR_SHL: return "shl"; + case FE_IR_SHR: return "shr"; + case FE_IR_EQ: return "eq"; + case FE_IR_NE: return "ne"; + case FE_IR_LT: return "lt"; + case FE_IR_LE: return "le"; + case FE_IR_GT: return "gt"; + case FE_IR_GE: return "ge"; + case FE_IR_CAST: return "cast"; + case FE_IR_CALL: return "call"; + case FE_IR_COPY: return "copy"; + } + return "?"; +} + +static const char *trap_name(FeIrTrap t) +{ + switch (t) { + case FE_TRAP_BOUNDS: return "bounds"; + case FE_TRAP_OVERFLOW: return "overflow"; + case FE_TRAP_DIVIDE: return "divide"; + case FE_TRAP_UNREACHABLE: return "unreachable"; + case FE_TRAP_EXPLICIT: return "trap"; + } + return "?"; +} + +static void dump_place(const FeIrPlace *p, FILE *out) +{ + switch (p->base) { + case FE_PLACE_LOCAL: fprintf(out, "$%u", p->index); break; + case FE_PLACE_GLOBAL: fprintf(out, "@%s", p->name ? p->name : "?"); break; + case FE_PLACE_TEMP: fprintf(out, "%%%u", p->index); break; + } + if (p->offset) fprintf(out, " + %ld", p->offset); +} + +static void dump_value(const FeIrValue *v, FILE *out) +{ + unsigned i; + fputs(" ", out); + if (v->has_dest) fprintf(out, "%%%u = ", v->dest); + switch (v->op) { + case FE_IR_CONST: + fprintf(out, "const %s %ld", fe_ir_type_name(v->type), v->imm); + break; + case FE_IR_LOAD: + fprintf(out, "load %s ", fe_ir_type_name(v->type)); + dump_place(&v->place, out); + break; + case FE_IR_STORE: + fputs("store ", out); + dump_place(&v->place, out); + fprintf(out, ", %%%u", v->a); + break; + case FE_IR_ADDR: + fputs("addr ", out); + dump_place(&v->place, out); + break; + case FE_IR_CAST: + fprintf(out, "cast %s %s %%%u", + fe_ir_type_name((FeIrType)v->imm), + fe_ir_type_name(v->type), v->a); + break; + case FE_IR_CALL: + fprintf(out, "call @%s(", v->callee ? v->callee : "?"); + for (i = 0; i < v->arg_count; ++i) + fprintf(out, "%s%%%u", i ? ", " : "", v->args[i]); + fputc(')', out); + break; + case FE_IR_COPY: + fputs("copy ", out); + dump_place(&v->place, out); + fputs(", ", out); + dump_place(&v->place2, out); + fprintf(out, ", %ld", v->imm); + break; + default: + fprintf(out, "%s %s %%%u, %%%u", fe_ir_op_name(v->op), + fe_ir_type_name(v->op >= FE_IR_EQ && v->op <= FE_IR_GE ? + (FeIrType)v->imm : v->type), v->a, v->b); + if (v->is_unsigned) fputs(" u", out); + break; + } + fputc('\n', out); +} + +void fe_ir_dump(const FeIrModule *m, FILE *out) +{ + const FeIrFunc *f; + const FeIrBlock *b; + const FeIrValue *v; + const FeIrGlobal *g; + unsigned i; + if (m->unit_file && m->unit_file[0]) + fprintf(out, "; unit file %s\n", m->unit_file); + for (g = m->globals; g; g = g->next) + fprintf(out, "global @%s : %s %lu\n", g->name, + fe_ir_type_name(g->type), g->size); + for (f = m->funcs; f; f = f->next) { + if (f->is_extern) { + fprintf(out, "extern fn @%s -> %s\n", f->name, + fe_ir_type_name(f->ret)); + continue; + } + fprintf(out, "fn @%s -> %s%s {\n", f->name, fe_ir_type_name(f->ret), + f->returns_by_address ? " (by address)" : ""); + for (i = 0; i < f->local_count; ++i) { + fprintf(out, " $%u: %s", i, fe_ir_type_name(f->locals[i].type)); + if (f->locals[i].type == FE_IR_MEM) + fprintf(out, "<%lu>", f->locals[i].size); + if (i < f->param_count) fputs(" ; parameter", out); + if (f->locals[i].name) fprintf(out, " ; %s", f->locals[i].name); + fputc('\n', out); + } + for (b = f->first; b; b = b->next) { + fprintf(out, " b%u:\n", b->id); + for (v = b->first; v; v = v->next) dump_value(v, out); + switch (b->term) { + case FE_IR_JMP: + fprintf(out, " jmp b%u\n", b->target); break; + case FE_IR_BR: + fprintf(out, " br %%%u, b%u, b%u\n", b->cond, b->target, + b->target_else); break; + case FE_IR_RET: + if (b->has_ret_value) fprintf(out, " ret %%%u\n", b->ret_value); + else fputs(" ret\n", out); + break; + case FE_IR_TRAP: + fprintf(out, " trap %s %lu\n", trap_name(b->trap), + b->trap_line); + break; + } + } + fputs("}\n", out); + } +} diff --git a/fec/src/ir.h b/fec/src/ir.h new file mode 100644 index 0000000..9f10834 --- /dev/null +++ b/fec/src/ir.h @@ -0,0 +1,177 @@ +#ifndef FE_IR_H +#define FE_IR_H + +#include "arena.h" +#include + +/* The intermediate representation. `IR.md` is the description; this is the + shape it takes in memory. + + Ferro types do not survive into here. A struct, a slice, an optional and an + error union are all `mem`, and a field is a byte offset that lowering + worked out. The machine types are what a register can hold plus a size. */ + +typedef enum FeIrType { + FE_IR_VOID, + FE_IR_I8, FE_IR_I16, FE_IR_I32, + FE_IR_PTR, + FE_IR_MEM /* size lives on the value or slot */ +} FeIrType; + +typedef enum FeIrOp { + FE_IR_CONST, FE_IR_LOAD, FE_IR_STORE, FE_IR_ADDR, + FE_IR_ADD, FE_IR_SUB, FE_IR_MUL, FE_IR_DIV, FE_IR_MOD, + FE_IR_AND, FE_IR_OR, FE_IR_XOR, FE_IR_SHL, FE_IR_SHR, + FE_IR_EQ, FE_IR_NE, FE_IR_LT, FE_IR_LE, FE_IR_GT, FE_IR_GE, + FE_IR_CAST, FE_IR_CALL, FE_IR_COPY +} FeIrOp; + +typedef enum FeIrTerm { + FE_IR_JMP, FE_IR_BR, FE_IR_RET, FE_IR_TRAP +} FeIrTerm; + +/* Why a program stopped. Kept small and stable: it is a number in the + executable, and the runtime turns it back into words. */ +typedef enum FeIrTrap { + FE_TRAP_BOUNDS = 0, + FE_TRAP_OVERFLOW = 1, + FE_TRAP_DIVIDE = 2, + FE_TRAP_UNREACHABLE = 3, + FE_TRAP_EXPLICIT = 4 +} FeIrTrap; + +/* Where an instruction reads or writes. A place is a base plus a constant + offset; anything computed goes through a pointer temporary instead. */ +typedef enum FeIrBase { + FE_PLACE_LOCAL, /* $n */ + FE_PLACE_GLOBAL, /* @name */ + FE_PLACE_TEMP /* %p */ +} FeIrBase; + +typedef struct FeIrPlace { + FeIrBase base; + unsigned index; /* local or temp number */ + const char *name; /* global name */ + long offset; +} FeIrPlace; + +typedef struct FeIrValue { + FeIrOp op; + FeIrType type; + unsigned dest; /* %dest, or 0 when the op has no result */ + int has_dest; + /* operands, by role -- only the ones the op uses are set */ + unsigned a, b; /* temporaries */ + long imm; /* const, cast width, copy size */ + FeIrPlace place; /* load / store / addr / copy destination */ + FeIrPlace place2; /* copy source */ + const char *callee; + unsigned *args; + unsigned arg_count; + int is_unsigned; /* picks the signed or unsigned instruction */ + unsigned long line; /* for diagnostics that survive into the backend */ + struct FeIrValue *next; +} FeIrValue; + +typedef struct FeIrFunc FeIrFunc; + +typedef struct FeIrBlock { + unsigned id; + FeIrFunc *func; /* the function this block is being built in */ + FeIrValue *first; + FeIrValue *last; + FeIrTerm term; + unsigned cond; /* br */ + unsigned target; /* jmp, br true */ + unsigned target_else; /* br false */ + unsigned ret_value; /* ret */ + int has_ret_value; + FeIrTrap trap; + unsigned long trap_line; + struct FeIrBlock *next; +} FeIrBlock; + +typedef struct FeIrLocal { + FeIrType type; + unsigned long size; /* for FE_IR_MEM */ + unsigned align; + const char *name; /* the Ferro name, for reading the dump */ +} FeIrLocal; + +struct FeIrFunc { + const char *name; /* unit.name */ + FeIrType ret; + unsigned long ret_size; /* when ret is FE_IR_MEM */ + /* A function returning mem takes the address to write as a hidden + first parameter, so the caller owns the storage. */ + int returns_by_address; + FeIrLocal *locals; + unsigned local_count; + unsigned local_capacity; + unsigned param_count; /* the first `param_count` locals are parameters */ + unsigned temp_count; + FeIrBlock *first; + FeIrBlock *last; + unsigned block_count; + int is_extern; + struct FeIrFunc *next; +}; + +typedef struct FeIrGlobal { + const char *name; + FeIrType type; + unsigned long size; + unsigned align; + const unsigned char *init; /* size bytes, or null for zero */ + struct FeIrGlobal *next; +} FeIrGlobal; + +typedef struct FeIrModule { + FeArena arena; + const char *unit_file; /* the one file-name string a unit's traps share */ + FeIrFunc *funcs; + FeIrFunc *last_func; + FeIrGlobal *globals; + FeIrGlobal *last_global; +} FeIrModule; + +void fe_ir_module_init(FeIrModule *m); +void fe_ir_module_destroy(FeIrModule *m); + +FeIrFunc *fe_ir_func(FeIrModule *m, const char *name, FeIrType ret, + unsigned long ret_size); +unsigned fe_ir_local(FeIrModule *m, FeIrFunc *f, FeIrType type, + unsigned long size, unsigned align, const char *name); +unsigned fe_ir_temp(FeIrFunc *f); +FeIrBlock *fe_ir_block(FeIrModule *m, FeIrFunc *f); + +/* Places */ +FeIrPlace fe_ir_at_local(unsigned index, long offset); +FeIrPlace fe_ir_at_global(const char *name, long offset); +FeIrPlace fe_ir_at_temp(unsigned temp, long offset); + +/* Instructions. Each returns the destination temporary where there is one. */ +unsigned fe_ir_const(FeIrModule *m, FeIrBlock *b, FeIrType t, long v); +unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p); +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned v); +unsigned fe_ir_addr(FeIrModule *m, FeIrBlock *b, FeIrPlace p); +unsigned fe_ir_binary(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t, + unsigned a, unsigned c, int is_unsigned); +unsigned fe_ir_cast(FeIrModule *m, FeIrBlock *b, FeIrType from, FeIrType to, + unsigned a, int is_unsigned); +unsigned fe_ir_call(FeIrModule *m, FeIrBlock *b, FeIrType ret, + const char *callee, unsigned *args, unsigned count); +void fe_ir_copy(FeIrModule *m, FeIrBlock *b, FeIrPlace dst, FeIrPlace src, + unsigned long size); + +/* Terminators */ +void fe_ir_jmp(FeIrBlock *b, unsigned target); +void fe_ir_br(FeIrBlock *b, unsigned cond, unsigned t, unsigned f); +void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value); +void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line); + +void fe_ir_dump(const FeIrModule *m, FILE *out); +const char *fe_ir_type_name(FeIrType t); +const char *fe_ir_op_name(FeIrOp op); + +#endif diff --git a/tests/run.py b/tests/run.py index ff66496..aa53325 100644 --- a/tests/run.py +++ b/tests/run.py @@ -34,7 +34,7 @@ ROOT = Path(__file__).resolve().parent.parent FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", - "check", "resolve", "driver") + "check", "resolve", "ir", "driver") # Fixtures live here until there is a code generator to run them against. QUARANTINE = "pending-backend" From 6ee37646670942e266ffef82ffb027dce557b9f1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:51:01 +0900 Subject: [PATCH 136/184] =?UTF-8?q?lower:=20=EA=B2=80=EC=82=AC=EB=90=9C=20?= =?UTF-8?q?AST=20=EB=A5=BC=20IR=20=EB=A1=9C=20=EB=82=B4=EB=A6=B0=EB=8B=A4?= =?UTF-8?q?=20(1=EC=B0=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 함수, 파라미터, 지역, 리터럴, 이름, 산술과 비교, 대입, if, while, break/continue, 호출, 참조, 필드 접근, 포인터 역참조까지. Slot 이 표현식의 결과다 -- 임시값에 든 값이거나 메모리 안의 자리다. 덩어리는 언제나 자리다. 임시값은 레지스터이고 덩어리는 거기 안 들어가기 때문이다. and/or 는 연산이 아니라 제어 흐름으로 내린다. 오른쪽을 평가하지 않아야 하는 경우가 있어서다. 블록에 terminated 플래그를 뒀다. 없으면 분기 안의 return 이 join 으로 가는 점프에 덮인다. --dump-ir 로 볼 수 있다. try/catch/defer/drop/for/배열/옵셔널/에러유니온과 제네릭 인스턴스는 아직이다. --- fec/src/driver.c | 15 +- fec/src/ir.c | 8 + fec/src/ir.h | 4 + fec/src/lower.c | 588 +++++++++++++++++++++++++++++++++++++++++++++++ fec/src/lower.h | 18 ++ tests/run.py | 2 +- 6 files changed, 631 insertions(+), 4 deletions(-) create mode 100644 fec/src/lower.c create mode 100644 fec/src/lower.h diff --git a/fec/src/driver.c b/fec/src/driver.c index 91dc7cf..cd18a5e 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -1,6 +1,7 @@ #include "parser.h" #include "check.h" #include "resolve.h" +#include "lower.h" #include #include #include @@ -16,7 +17,7 @@ static char *read_file(const char *name, unsigned long *size) static void usage(void) { - puts("usage: fec [--dump-tokens|--dump-ast|--check] file.fe [--no-checks]"); + puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir] file.fe [--no-checks]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -37,7 +38,7 @@ static void dump_tokens(const char *src, unsigned long n, const char *file, int main(int argc, char **argv) { - int i,dump=0,dump_tok=0,check_only=0,no_checks=0; + int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0; const char *file=0; unsigned long n; char *src; @@ -51,13 +52,14 @@ int main(int argc, char **argv) if(strcmp(argv[i],"--dump-ast")==0) dump=1; else if(strcmp(argv[i],"--dump-tokens")==0) dump_tok=1; else if(strcmp(argv[i],"--check")==0) check_only=1; + else if(strcmp(argv[i],"--dump-ir")==0) dump_ir=1; else if(strcmp(argv[i],"--no-checks")==0) no_checks=1; else if(strncmp(argv[i],"--target=",9)==0 || strncmp(argv[i],"--model=",8)==0 || strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} else {fprintf(fe_diag_stream(),"fec: unknown option %s\n",argv[i]);return 2;} } - if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)>1){ + if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)+(dump_ir?1:0)>1){ fprintf(fe_diag_stream(),"fec: choose only one output mode\n"); return 2; } @@ -92,6 +94,13 @@ int main(int argc, char **argv) if(ok){ fe_check_init(&check,&build,&d,pointer_bits,no_checks); if(!fe_check_program(&check)) ok=0; + if(ok && dump_ir){ + FeIrModule ir; + fe_ir_module_init(&ir); + if(!fe_lower_program(&check,&ir)) ok=0; + else fe_ir_dump(&ir,stdout); + fe_ir_module_destroy(&ir); + } fe_check_destroy(&check); } fe_build_destroy(&build); diff --git a/fec/src/ir.c b/fec/src/ir.c index bff4452..aa21351 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -215,12 +215,16 @@ void fe_ir_copy(FeIrModule *m, FeIrBlock *b, FeIrPlace dst, FeIrPlace src, void fe_ir_jmp(FeIrBlock *b, unsigned target) { + if (b->terminated) return; + b->terminated = 1; b->term = FE_IR_JMP; b->target = target; } void fe_ir_br(FeIrBlock *b, unsigned cond, unsigned t, unsigned f) { + if (b->terminated) return; + b->terminated = 1; b->term = FE_IR_BR; b->cond = cond; b->target = t; @@ -229,6 +233,8 @@ void fe_ir_br(FeIrBlock *b, unsigned cond, unsigned t, unsigned f) void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value) { + if (b->terminated) return; + b->terminated = 1; b->term = FE_IR_RET; b->ret_value = value; b->has_ret_value = has_value; @@ -236,6 +242,8 @@ void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value) void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line) { + if (b->terminated) return; + b->terminated = 1; b->term = FE_IR_TRAP; b->trap = reason; b->trap_line = line; diff --git a/fec/src/ir.h b/fec/src/ir.h index 9f10834..2829f95 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -88,6 +88,10 @@ typedef struct FeIrBlock { int has_ret_value; FeIrTrap trap; unsigned long trap_line; + /* Set once a terminator is chosen. Lowering asks before appending a + jump, so a `return` inside a branch is not overwritten by the jump + to the join block. */ + int terminated; struct FeIrBlock *next; } FeIrBlock; diff --git a/fec/src/lower.c b/fec/src/lower.c new file mode 100644 index 0000000..ac9b95b --- /dev/null +++ b/fec/src/lower.c @@ -0,0 +1,588 @@ +#include "lower.h" +#include +#include + +/* ------------------------------------------------------------------------- * + * Lowering + * + * One function at a time, one statement at a time. A `Slot` is what an + * expression produced: either a value already in a temporary, or a place in + * memory that a value can be read from or written to. Aggregates are always + * places -- they are never carried in a temporary, because a temporary is a + * register and an aggregate does not fit in one. + * ------------------------------------------------------------------------- */ + +#define LOWER_MAX_LOCALS 256 + +typedef struct LowerVar { + const char *cname; + unsigned local; +} LowerVar; + +typedef struct Lower { + FeCheck *c; + FeIrModule *m; + FeIrFunc *fn; + FeIrBlock *b; /* the block being appended to */ + FeType *ret_type; + unsigned ret_local; /* hidden result address, when returning mem */ + LowerVar vars[LOWER_MAX_LOCALS]; + unsigned var_count; + /* Loop targets, for break and continue. */ + unsigned break_target[32]; + unsigned continue_target[32]; + unsigned loop_depth; + int failed; +} Lower; + +typedef struct Slot { + int is_place; + unsigned temp; /* the value, when is_place is 0 */ + FeIrPlace place; /* where it lives, when is_place is 1 */ + FeIrType type; + unsigned long size; /* for FE_IR_MEM */ +} Slot; + +static Slot lower_expr(Lower *L, FeNode *n); +static void lower_stmt(Lower *L, FeNode *n); + +static void fail(Lower *L, const char *why, FeNode *n) +{ + if (L->failed) return; + L->failed = 1; + fprintf(fe_diag_stream(), "%s:%lu:%lu: internal: cannot lower %s\n", + n && n->loc.file ? n->loc.file : "?", + n ? n->loc.line : 0UL, n ? n->loc.col : 0UL, why); +} + +/* ---------------------------------------------------------------- types --- */ + +/* A Ferro type becomes what a register can hold, or a size in memory. Anything + with more than one field is memory: the backend never has to decide whether + an aggregate fits somewhere. */ +static FeIrType ir_type_of(const FeType *t) +{ + if (!t) return FE_IR_VOID; + switch (t->kind) { + case FE_TYPE_VOID: return FE_IR_VOID; + case FE_TYPE_BOOL: + case FE_TYPE_CHAR: return FE_IR_I8; + case FE_TYPE_INT: + if (t->bits <= 8U) return FE_IR_I8; + if (t->bits <= 16U) return FE_IR_I16; + return FE_IR_I32; + case FE_TYPE_REF: return FE_IR_PTR; + case FE_TYPE_OWNED: + /* An owned slice carries a length beside the pointer. */ + return t->elem && t->elem->kind == FE_TYPE_SLICE ? FE_IR_MEM : FE_IR_PTR; + case FE_TYPE_ENUM: + /* A payload-free enum is just its tag. */ + return t->variant_count && t->fields ? FE_IR_MEM : + (t->size <= 1UL ? FE_IR_I8 : + t->size <= 2UL ? FE_IR_I16 : FE_IR_I32); + default: + return FE_IR_MEM; + } +} + +static int enum_has_payload(const FeType *t) +{ + unsigned i; + if (!t || t->kind != FE_TYPE_ENUM) return 0; + for (i = 0; i < t->variant_count; ++i) + if (t->variants[i].field_count) return 1; + return 0; +} + +static FeIrType ir_type(const FeType *t) +{ + if (t && t->kind == FE_TYPE_ENUM && enum_has_payload(t)) return FE_IR_MEM; + return ir_type_of(t); +} + +static unsigned long ir_size(const FeType *t) +{ + return t ? fe_type_size(t) : 0UL; +} + +static unsigned ir_align(const FeType *t) +{ + return t ? fe_type_align(t) : 1U; +} + +static int type_is_unsigned(const FeType *t) +{ + return t && t->kind == FE_TYPE_INT && t->is_unsigned; +} + +/* ---------------------------------------------------------------- slots --- */ + +static Slot slot_value(unsigned temp, FeIrType t) +{ + Slot s; + s.is_place = 0; s.temp = temp; s.type = t; s.size = 0; + s.place = fe_ir_at_temp(0, 0); + return s; +} + +static Slot slot_place(FeIrPlace p, FeIrType t, unsigned long size) +{ + Slot s; + s.is_place = 1; s.temp = 0; s.place = p; s.type = t; s.size = size; + return s; +} + +static Slot slot_void(void) +{ + return slot_value(0, FE_IR_VOID); +} + +/* Read a slot as a value. An aggregate has no value form, so asking for one is + a lowering bug rather than a program error. */ +static unsigned as_value(Lower *L, Slot s, FeNode *n) +{ + if (!s.is_place) return s.temp; + if (s.type == FE_IR_MEM) { fail(L, "an aggregate as a value", n); return 0; } + return fe_ir_load(L->m, L->b, s.type, s.place); +} + +/* The address of a slot. */ +static unsigned as_address(Lower *L, Slot s, FeNode *n) +{ + if (!s.is_place) { fail(L, "the address of a temporary", n); return 0; } + return fe_ir_addr(L->m, L->b, s.place); +} + +/* --------------------------------------------------------------- locals --- */ + +static unsigned declare_var(Lower *L, const char *cname, const FeType *t, + const char *name) +{ + unsigned local = fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), + ir_align(t), name); + if (L->var_count < LOWER_MAX_LOCALS) { + L->vars[L->var_count].cname = cname; + L->vars[L->var_count].local = local; + ++L->var_count; + } + return local; +} + +static int find_var(Lower *L, const char *cname, unsigned *out) +{ + unsigned i; + if (!cname) return 0; + for (i = L->var_count; i > 0; --i) + if (L->vars[i - 1].cname && strcmp(L->vars[i - 1].cname, cname) == 0) { + *out = L->vars[i - 1].local; + return 1; + } + return 0; +} + +/* --------------------------------------------------------------- blocks --- */ + +static FeIrBlock *new_block(Lower *L) +{ + return fe_ir_block(L->m, L->fn); +} + +/* ---------------------------------------------------------- expressions --- */ + +static FeIrOp binary_op(const char *op, int *is_cmp) +{ + *is_cmp = 0; + if (!op) return FE_IR_ADD; + if (!strcmp(op, "+") || !strcmp(op, "+%")) return FE_IR_ADD; + if (!strcmp(op, "-") || !strcmp(op, "-%")) return FE_IR_SUB; + if (!strcmp(op, "*") || !strcmp(op, "*%")) return FE_IR_MUL; + if (!strcmp(op, "/")) return FE_IR_DIV; + if (!strcmp(op, "%")) return FE_IR_MOD; + if (!strcmp(op, "&")) return FE_IR_AND; + if (!strcmp(op, "|")) return FE_IR_OR; + if (!strcmp(op, "^")) return FE_IR_XOR; + if (!strcmp(op, "<<")) return FE_IR_SHL; + if (!strcmp(op, ">>")) return FE_IR_SHR; + *is_cmp = 1; + if (!strcmp(op, "==")) return FE_IR_EQ; + if (!strcmp(op, "!=")) return FE_IR_NE; + if (!strcmp(op, "<")) return FE_IR_LT; + if (!strcmp(op, "<=")) return FE_IR_LE; + if (!strcmp(op, ">")) return FE_IR_GT; + if (!strcmp(op, ">=")) return FE_IR_GE; + *is_cmp = 0; + return FE_IR_ADD; +} + +static long literal_value(FeNode *n) +{ + const char *s = n->text; + long v = 0; + int neg = 0; + if (!s) return 0; + if (!strcmp(s, "true")) return 1; + if (!strcmp(s, "false")) return 0; + if (!strcmp(s, "null") || !strcmp(s, "undefined")) return 0; + if (*s == '\'') { + /* A character literal; the lexer kept the quotes. */ + if (s[1] == '\\') { + switch (s[2]) { + case 'n': return 10; + case 't': return 9; + case 'r': return 13; + case '0': return 0; + default: return (long)(unsigned char)s[2]; + } + } + return (long)(unsigned char)s[1]; + } + if (*s == '-') { neg = 1; ++s; } + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { + s += 2; + for (; *s; ++s) { + int d = *s >= '0' && *s <= '9' ? *s - '0' : + *s >= 'a' && *s <= 'f' ? *s - 'a' + 10 : + *s >= 'A' && *s <= 'F' ? *s - 'A' + 10 : -1; + if (d < 0) { if (*s == '_') continue; break; } + v = v * 16 + d; + } + } else { + for (; *s; ++s) { + if (*s == '_') continue; + if (*s < '0' || *s > '9') break; + v = v * 10 + (*s - '0'); + } + } + return neg ? -v : v; +} + +/* `and` and `or` do not evaluate the right side unless they have to, so they + are control flow rather than an operation. */ +static Slot lower_logical(Lower *L, FeNode *n, int is_and) +{ + unsigned result = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "logical"); + FeIrBlock *rhs = new_block(L); + FeIrBlock *join = new_block(L); + FeIrBlock *entry = L->b; + unsigned left; + unsigned right; + L->b = entry; + left = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), left); + if (is_and) fe_ir_br(L->b, left, rhs->id, join->id); + else fe_ir_br(L->b, left, join->id, rhs->id); + L->b = rhs; + right = as_value(L, lower_expr(L, n->b), n->b); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), right); + fe_ir_jmp(L->b, join->id); + L->b = join; + return slot_place(fe_ir_at_local(result, 0), FE_IR_I8, 1); +} + +static Slot lower_call(Lower *L, FeNode *n) +{ + unsigned args[16]; + unsigned count = 0; + FeNode *arg; + FeType *ret = n->sem_type; + FeIrType rt = ir_type(ret); + unsigned result_local = 0; + const char *callee = n->a && n->a->cname ? n->a->cname : + (n->sem_decl && n->sem_decl->cname ? + n->sem_decl->cname : 0); + if (!callee) { fail(L, "a call with no target", n); return slot_void(); } + /* An aggregate result is written through a hidden first argument. */ + if (rt == FE_IR_MEM) { + result_local = fe_ir_local(L->m, L->fn, FE_IR_MEM, ir_size(ret), + ir_align(ret), "result"); + args[count++] = fe_ir_addr(L->m, L->b, fe_ir_at_local(result_local, 0)); + } + for (arg = n->children; arg; arg = arg->next) { + Slot a = lower_expr(L, arg); + if (count >= 16) { fail(L, "too many arguments", n); break; } + args[count++] = a.type == FE_IR_MEM ? as_address(L, a, arg) + : as_value(L, a, arg); + } + if (rt == FE_IR_MEM) { + fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); + return slot_place(fe_ir_at_local(result_local, 0), FE_IR_MEM, + ir_size(ret)); + } + if (rt == FE_IR_VOID) { + fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); + return slot_void(); + } + return slot_value(fe_ir_call(L->m, L->b, rt, callee, args, count), rt); +} + +static Slot lower_expr(Lower *L, FeNode *n) +{ + FeType *t; + FeIrType it; + if (!n || L->failed) return slot_void(); + t = n->sem_type; + it = ir_type(t); + switch (n->kind) { + case FE_N_LITERAL: + return slot_value(fe_ir_const(L->m, L->b, + it == FE_IR_VOID ? FE_IR_I32 : it, + literal_value(n)), + it == FE_IR_VOID ? FE_IR_I32 : it); + case FE_N_IDENT: { + unsigned local; + if (find_var(L, n->cname, &local)) + return slot_place(fe_ir_at_local(local, 0), it, ir_size(t)); + if (n->cname) + return slot_place(fe_ir_at_global(n->cname, 0), it, ir_size(t)); + fail(L, "an unresolved name", n); + return slot_void(); + } + case FE_N_BINARY: { + int is_cmp = 0; + FeIrOp op; + unsigned a; + unsigned b; + FeIrType operand; + if (n->text && (!strcmp(n->text, "and") || !strcmp(n->text, "or"))) + return lower_logical(L, n, !strcmp(n->text, "and")); + op = binary_op(n->text, &is_cmp); + operand = ir_type(n->a ? n->a->sem_type : 0); + if (operand == FE_IR_VOID || operand == FE_IR_MEM) operand = FE_IR_I32; + a = as_value(L, lower_expr(L, n->a), n->a); + b = as_value(L, lower_expr(L, n->b), n->b); + return slot_value(fe_ir_binary(L->m, L->b, op, operand, a, b, + type_is_unsigned(n->a ? n->a->sem_type + : 0)), + is_cmp ? FE_IR_I8 : operand); + } + case FE_N_UNARY: + if (n->text && !strcmp(n->text, "-")) { + unsigned zero = fe_ir_const(L->m, L->b, it, 0); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_SUB, it, zero, v, + 0), it); + } + if (n->text && !strcmp(n->text, "not")) { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I8, v, + zero, 0), FE_IR_I8); + } + if (n->text && (!strcmp(n->text, "&") || !strcmp(n->text, "&mut"))) { + Slot inner = lower_expr(L, n->a); + return slot_value(as_address(L, inner, n->a), FE_IR_PTR); + } + fail(L, "this unary operator", n); + return slot_void(); + case FE_N_MEMBER: + /* `p.^` reads through a pointer. */ + if (n->text && !strcmp(n->text, ".^")) { + unsigned p = as_value(L, lower_expr(L, n->a), n->a); + return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); + } + /* A field is a constant offset from the base. */ + { + FeType *base = n->a ? n->a->sem_type : 0; + FeFieldType *field; + Slot b; + if (base && (base->kind == FE_TYPE_REF || + base->kind == FE_TYPE_OWNED)) base = base->elem; + field = fe_type_field(base, n->b && n->b->text ? n->b->text : ""); + if (!field) { fail(L, "an unresolved field", n); return slot_void(); } + b = lower_expr(L, n->a); + if (n->a->sem_type && (n->a->sem_type->kind == FE_TYPE_REF || + n->a->sem_type->kind == FE_TYPE_OWNED)) { + unsigned p = as_value(L, b, n->a); + return slot_place(fe_ir_at_temp(p, (long)field->offset), it, + ir_size(t)); + } + if (!b.is_place) { fail(L, "a field of a temporary", n); return slot_void(); } + b.place.offset += (long)field->offset; + return slot_place(b.place, it, ir_size(t)); + } + case FE_N_CALL: + return lower_call(L, n); + case FE_N_EXPR: + return lower_expr(L, n->a); + default: + fail(L, "this expression", n); + return slot_void(); + } +} + +/* ----------------------------------------------------------- statements --- */ + +static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, + unsigned long size) +{ + if (value.type == FE_IR_MEM) { + if (!value.is_place) { fail(L, "an aggregate value", n); return; } + fe_ir_copy(L->m, L->b, dst, value.place, size); + return; + } + fe_ir_store(L->m, L->b, dst, as_value(L, value, n)); +} + +static void lower_return(Lower *L, FeNode *n) +{ + Slot v; + if (!n->a) { fe_ir_ret(L->b, 0, 0); return; } + v = lower_expr(L, n->a); + if (L->fn->returns_by_address) { + store_into(L, fe_ir_at_temp(L->ret_local, 0), v, n, + ir_size(L->ret_type)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, as_value(L, v, n->a), 1); +} + +static void lower_if(Lower *L, FeNode *n) +{ + FeIrBlock *then_b = new_block(L); + FeIrBlock *else_b = n->c ? new_block(L) : 0; + FeIrBlock *join = new_block(L); + unsigned cond = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_br(L->b, cond, then_b->id, else_b ? else_b->id : join->id); + L->b = then_b; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, join->id); + if (else_b) { + L->b = else_b; + lower_stmt(L, n->c); + fe_ir_jmp(L->b, join->id); + } + L->b = join; +} + +static void lower_while(Lower *L, FeNode *n) +{ + FeIrBlock *head = new_block(L); + FeIrBlock *body = new_block(L); + FeIrBlock *done = new_block(L); + unsigned cond; + fe_ir_jmp(L->b, head->id); + L->b = head; + cond = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_br(L->b, cond, body->id, done->id); + if (L->loop_depth < 32) { + L->break_target[L->loop_depth] = done->id; + L->continue_target[L->loop_depth] = head->id; + ++L->loop_depth; + } + L->b = body; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, head->id); + if (L->loop_depth) --L->loop_depth; + L->b = done; +} + +static void lower_stmt(Lower *L, FeNode *n) +{ + FeNode *x; + if (!n || L->failed) return; + switch (n->kind) { + case FE_N_BLOCK: + for (x = n->children; x; x = x->next) lower_stmt(L, x); + return; + case FE_N_LET: + case FE_N_VAR: + case FE_N_CONST: { + unsigned local = declare_var(L, n->cname, n->sem_type, n->text); + if (n->b) { + Slot v = lower_expr(L, n->b); + store_into(L, fe_ir_at_local(local, 0), v, n, ir_size(n->sem_type)); + } + return; + } + case FE_N_ASSIGN: { + Slot dst = lower_expr(L, n->a); + Slot v = lower_expr(L, n->b); + if (!dst.is_place) { fail(L, "an assignment to a value", n); return; } + store_into(L, dst.place, v, n, dst.size); + return; + } + case FE_N_EXPR_STMT: + lower_expr(L, n->a); + return; + case FE_N_RETURN: + lower_return(L, n); + return; + case FE_N_IF: + lower_if(L, n); + return; + case FE_N_WHILE: + lower_while(L, n); + return; + case FE_N_BREAK: + if (L->loop_depth) fe_ir_jmp(L->b, L->break_target[L->loop_depth - 1]); + return; + case FE_N_CONTINUE: + if (L->loop_depth) + fe_ir_jmp(L->b, L->continue_target[L->loop_depth - 1]); + return; + case FE_N_UNSAFE: + lower_stmt(L, n->a); + return; + default: + fail(L, "this statement", n); + return; + } +} + +/* ------------------------------------------------------------ functions --- */ + +static void lower_fn(Lower *L, FeNode *fn) +{ + FeNode *p; + FeType *ret = fn->b ? fe_type_from_ast(&L->c->types, fn->b) : 0; + FeIrFunc *f; + if (!fn->cname) return; + f = fe_ir_func(L->m, fn->cname, ir_type(ret), ir_size(ret)); + if (!f) return; + L->fn = f; + L->ret_type = ret; + L->var_count = 0; + L->loop_depth = 0; + /* A hidden first parameter holds where an aggregate result goes. */ + if (f->returns_by_address) + L->ret_local = fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, "result"); + for (p = fn->a ? fn->a->children : 0; p; p = p->next) { + FeType *pt = fe_type_from_ast(&L->c->types, p->a); + /* An aggregate parameter arrives as an address. */ + unsigned local = ir_type(pt) == FE_IR_MEM + ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) + : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), + p->text); + if (L->var_count < LOWER_MAX_LOCALS) { + L->vars[L->var_count].cname = p->cname; + L->vars[L->var_count].local = local; + ++L->var_count; + } + } + f->param_count = f->local_count; + L->b = fe_ir_block(L->m, f); + lower_stmt(L, fn->c); + /* A void function may just run off the end. */ + fe_ir_ret(L->b, 0, 0); +} + +int fe_lower_program(FeCheck *c, FeIrModule *out) +{ + Lower L; + unsigned u; + FeNode *n; + memset(&L, 0, sizeof L); + L.c = c; + L.m = out; + for (u = 0; u < c->build->count; ++u) { + FeUnit *unit = &c->build->units[u]; + c->ast = &unit->ast; + c->unit = unit; + c->types.unit_name = unit->name[0] ? unit->name : "unit"; + if (!out->unit_file || !out->unit_file[0]) out->unit_file = unit->path; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) + if (n->kind == FE_N_FN && n->c) lower_fn(&L, n); + } + return !L.failed; +} diff --git a/fec/src/lower.h b/fec/src/lower.h new file mode 100644 index 0000000..2d0c3dc --- /dev/null +++ b/fec/src/lower.h @@ -0,0 +1,18 @@ +#ifndef FE_LOWER_H +#define FE_LOWER_H + +#include "check.h" +#include "ir.h" + +/* Turn the checked program into IR. + + The checker leaves every expression with a type and every declaration with a + link-visible name; lowering reads those and produces the flat form the + backend wants. Everything Ferro-shaped is expanded here -- `try` becomes a + branch, `defer` is copied onto each exit path, an index becomes a comparison + and a trap -- so that neither the checker nor the backend has to know about + the other's world. */ + +int fe_lower_program(FeCheck *c, FeIrModule *out); + +#endif diff --git a/tests/run.py b/tests/run.py index aa53325..7f841ba 100644 --- a/tests/run.py +++ b/tests/run.py @@ -34,7 +34,7 @@ ROOT = Path(__file__).resolve().parent.parent FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", - "check", "resolve", "ir", "driver") + "check", "resolve", "ir", "lower", "driver") # Fixtures live here until there is a code generator to run them against. QUARANTINE = "pending-backend" From e5093e690dfdef0eb96c85a60d44d26f235cae16 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 05:55:34 +0900 Subject: [PATCH 137/184] =?UTF-8?q?backend:=20i386=20=EC=96=B4=EC=85=88?= =?UTF-8?q?=EB=B8=94=EB=A6=AC=EB=A5=BC=20=EB=82=B4=EA=B3=A0=20Windows=2011?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EB=A7=8C?= =?UTF-8?q?=EB=93=A0=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fec --emit-asm -> wasm -> wlink -> .exe. 툴체인은 고정된 Open Watcom 그대로다. 레지스터 할당기가 없다. 임시값마다 스택 슬롯을 주고, 명령마다 피연산자를 고정 레지스터로 읽어 계산하고 다시 저장한다. 느린 코드지만 명백히 옳은 코드이고, 옳은 것이 먼저다. 나중에 할당기를 끼워도 나머지는 모른다 -- 임시값이 어디 사는지만 바뀐다. 런타임 fec/rt/start.asm 은 진입 스텁과 fe_trap 이다. trap 은 이유와 파일과 줄을 stderr 에 쓰고 3으로 끝낸다. 처음으로 Ferro 프로그램이 실행됐다: 1..10 합 -> 55 (7*6-2)/4 -> 10 루프+호출+분기 -> 1 tests/build.py 가 컴파일하고 링크하고 돌린다. --- fec/rt/start.asm | 134 +++++++++++++++++ fec/src/driver.c | 19 ++- fec/src/ir.c | 1 + fec/src/ir.h | 3 + fec/src/lower.c | 7 +- fec/src/x86.c | 384 +++++++++++++++++++++++++++++++++++++++++++++++ fec/src/x86.h | 16 ++ tests/build.py | 103 +++++++++++++ tests/run.py | 2 +- 9 files changed, 662 insertions(+), 7 deletions(-) create mode 100644 fec/rt/start.asm create mode 100644 fec/src/x86.c create mode 100644 fec/src/x86.h create mode 100644 tests/build.py diff --git a/fec/rt/start.asm b/fec/rt/start.asm new file mode 100644 index 0000000..9d6860b --- /dev/null +++ b/fec/rt/start.asm @@ -0,0 +1,134 @@ +; Ferro runtime: process entry and the trap handler. +; +; The entry point calls the program's `main` and hands its result to +; ExitProcess, so a Ferro program is an ordinary console executable. +; `fe_trap` prints where the program stopped and why, then exits 3. + +.386 +.model flat + +extern _ExitProcess@4 : near +extern _GetStdHandle@4 : near +extern _WriteFile@20 : near +extern fe_main_ : near + +_DATA segment dword public 'DATA' + +reasons dd offset r_bounds, offset r_overflow, offset r_divide + dd offset r_unreach, offset r_explicit +r_bounds db 'index out of bounds',0 +r_overflow db 'integer overflow',0 +r_divide db 'divide by zero',0 +r_unreach db 'reached unreachable code',0 +r_explicit db 'trap',0 +r_unknown db 'trap',0 +prefix db 'ferro: ',0 +at_word db ' at ',0 +colon db ':',0 +newline db 13,10,0 +numbuf db 16 dup(0) +written dd 0 + +_DATA ends + +_TEXT segment dword public 'CODE' + +; write_cstr(esi = pointer to a NUL-terminated string) -> void +write_cstr proc near + push ebp + mov ebp, esp + push ebx + push esi + push edi + mov edi, esi + xor ecx, ecx +count_loop: + cmp byte ptr [edi], 0 + je count_done + inc edi + inc ecx + jmp count_loop +count_done: + test ecx, ecx + je write_done + push -11 ; STD_ERROR_HANDLE + call _GetStdHandle@4 + push 0 ; lpOverlapped + push offset written + push ecx + push esi + push eax + call _WriteFile@20 +write_done: + pop edi + pop esi + pop ebx + mov esp, ebp + pop ebp + ret +write_cstr endp + +; write_uint(eax = value) -> void +write_uint proc near + push ebp + mov ebp, esp + push ebx + mov edi, offset numbuf + 15 + mov byte ptr [edi], 0 + mov ebx, 10 +digit_loop: + xor edx, edx + div ebx + add dl, '0' + dec edi + mov [edi], dl + test eax, eax + jnz digit_loop + mov esi, edi + call write_cstr + pop ebx + mov esp, ebp + pop ebp + ret +write_uint endp + +; fe_trap(reason, file, line) -- cdecl, never returns +public fe_trap +fe_trap proc near + push ebp + mov ebp, esp + mov esi, offset prefix + call write_cstr + mov eax, [ebp+8] ; reason + cmp eax, 5 + jb reason_ok + mov esi, offset r_unknown + jmp reason_write +reason_ok: + mov esi, [reasons + eax*4] +reason_write: + call write_cstr + mov esi, offset at_word + call write_cstr + mov esi, [ebp+12] ; file + call write_cstr + mov esi, offset colon + call write_cstr + mov eax, [ebp+16] ; line + call write_uint + mov esi, offset newline + call write_cstr + push 3 + call _ExitProcess@4 +fe_trap endp + +public fe_start_ +fe_start_ proc near + call fe_main_ + push eax + call _ExitProcess@4 +fe_start_ endp + +_TEXT ends + +end fe_start_ diff --git a/fec/src/driver.c b/fec/src/driver.c index cd18a5e..7a15a81 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -2,6 +2,7 @@ #include "check.h" #include "resolve.h" #include "lower.h" +#include "x86.h" #include #include #include @@ -17,7 +18,7 @@ static char *read_file(const char *name, unsigned long *size) static void usage(void) { - puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir] file.fe [--no-checks]"); + puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--no-checks]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -38,8 +39,9 @@ static void dump_tokens(const char *src, unsigned long n, const char *file, int main(int argc, char **argv) { - int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0; + int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0,emit_asm=0; const char *file=0; + const char *out_path=0; unsigned long n; char *src; FeDiags d; @@ -53,13 +55,15 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"--dump-tokens")==0) dump_tok=1; else if(strcmp(argv[i],"--check")==0) check_only=1; else if(strcmp(argv[i],"--dump-ir")==0) dump_ir=1; + else if(strcmp(argv[i],"--emit-asm")==0) emit_asm=1; + else if(strcmp(argv[i],"-o")==0 && i+11){ + if((dump?1:0)+(dump_tok?1:0)+(check_only?1:0)+(dump_ir?1:0)+(emit_asm?1:0)>1){ fprintf(fe_diag_stream(),"fec: choose only one output mode\n"); return 2; } @@ -94,11 +98,16 @@ int main(int argc, char **argv) if(ok){ fe_check_init(&check,&build,&d,pointer_bits,no_checks); if(!fe_check_program(&check)) ok=0; - if(ok && dump_ir){ + if(ok && (dump_ir||emit_asm)){ FeIrModule ir; fe_ir_module_init(&ir); if(!fe_lower_program(&check,&ir)) ok=0; - else fe_ir_dump(&ir,stdout); + else if(dump_ir) fe_ir_dump(&ir,stdout); + else { + FILE *o=out_path?fopen(out_path,"w"):stdout; + if(!o){fprintf(fe_diag_stream(),"fec: cannot write %s\n",out_path);ok=0;} + else { fe_x86_emit(&ir,o); if(out_path) fclose(o); } + } fe_ir_module_destroy(&ir); } fe_check_destroy(&check); diff --git a/fec/src/ir.c b/fec/src/ir.c index aa21351..b5ebb20 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -5,6 +5,7 @@ void fe_ir_module_init(FeIrModule *m) { fe_arena_init(&m->arena, 16384); m->unit_file = ""; + m->entry_main = 0; m->funcs = 0; m->last_func = 0; m->globals = 0; diff --git a/fec/src/ir.h b/fec/src/ir.h index 2829f95..38b5acb 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -133,6 +133,9 @@ typedef struct FeIrGlobal { typedef struct FeIrModule { FeArena arena; const char *unit_file; /* the one file-name string a unit's traps share */ + /* The entry unit's `main`, if it has one. The runtime's start stub + calls a fixed name, so the generator emits a jump to this one. */ + const char *entry_main; FeIrFunc *funcs; FeIrFunc *last_func; FeIrGlobal *globals; diff --git a/fec/src/lower.c b/fec/src/lower.c index ac9b95b..23926ea 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -582,7 +582,12 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) c->types.unit_name = unit->name[0] ? unit->name : "unit"; if (!out->unit_file || !out->unit_file[0]) out->unit_file = unit->path; for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) - if (n->kind == FE_N_FN && n->c) lower_fn(&L, n); + if (n->kind == FE_N_FN && n->c) { + lower_fn(&L, n); + /* The entry unit is the one the build was rooted at. */ + if (u == 0 && n->text && !strcmp(n->text, "main")) + out->entry_main = n->cname; + } } return !L.failed; } diff --git a/fec/src/x86.c b/fec/src/x86.c new file mode 100644 index 0000000..11c6ef0 --- /dev/null +++ b/fec/src/x86.c @@ -0,0 +1,384 @@ +#include "x86.h" +#include + +/* ------------------------------------------------------------------------- * + * i386 code generation + * + * The frame, from EBP downwards: + * + * [ebp + 8 + 4k] incoming argument k + * [ebp + 4] return address + * [ebp] saved ebp + * [ebp - ...] parameters, copied in from the argument area + * [ebp - ...] locals + * [ebp - ...] one slot per temporary + * + * Parameters are copied into the frame rather than read in place so that a + * parameter and a local are the same thing to everything below. + * ------------------------------------------------------------------------- */ + +typedef struct Frame { + const FeIrFunc *f; + long *local_off; /* [ebp + off] for each local */ + long temp_base; /* first temporary slot */ + long size; /* bytes to subtract from esp */ +} Frame; + +static long align_up(long v, long a) +{ + long r = v % a; + return r ? v + a - r : v; +} + +static unsigned long slot_bytes(const FeIrLocal *l) +{ + switch (l->type) { + case FE_IR_I8: return 1; + case FE_IR_I16: return 2; + case FE_IR_I32: return 4; + case FE_IR_PTR: return 4; + case FE_IR_MEM: return l->size ? l->size : 1; + default: return 4; + } +} + +/* Every temporary is four bytes: a temporary only ever holds something that + fits in a register, and narrower values are kept zero- or sign-extended. */ +#define TEMP_SLOT 4L + +static void frame_layout(Frame *fr, const FeIrFunc *f, long *storage) +{ + unsigned i; + long off = 0; + fr->f = f; + fr->local_off = storage; + for (i = 0; i < f->local_count; ++i) { + unsigned long size = slot_bytes(&f->locals[i]); + long a = (long)f->locals[i].align; + if (a < 1) a = 1; + if (a > 4) a = 4; + off = align_up(off + (long)size, a); + storage[i] = -off; + } + off = align_up(off, 4); + fr->temp_base = -off; + off += (long)f->temp_count * TEMP_SLOT; + fr->size = align_up(off, 4); +} + +static long temp_off(const Frame *fr, unsigned t) +{ + return fr->temp_base - (long)(t + 1) * TEMP_SLOT; +} + +static const char *word_of(FeIrType t) +{ + switch (t) { + case FE_IR_I8: return "byte ptr"; + case FE_IR_I16: return "word ptr"; + default: return "dword ptr"; + } +} + +static const char *reg_of(FeIrType t, int which) +{ + /* which: 0 -> a, 1 -> c, 2 -> d */ + switch (t) { + case FE_IR_I8: return which == 0 ? "al" : which == 1 ? "cl" : "dl"; + case FE_IR_I16: return which == 0 ? "ax" : which == 1 ? "cx" : "dx"; + default: return which == 0 ? "eax" : which == 1 ? "ecx" : "edx"; + } +} + +/* Write the effective address of a place into `buf`. A place is a base plus a + constant, and the only base that is not already an address is a temporary, + which holds a pointer. */ +static void place_addr(const Frame *fr, const FeIrPlace *p, char *buf) +{ + switch (p->base) { + case FE_PLACE_LOCAL: + sprintf(buf, "[ebp%+ld]", fr->local_off[p->index] + p->offset); + break; + case FE_PLACE_GLOBAL: + if (p->offset) sprintf(buf, "[%s%+ld]", p->name, p->offset); + else sprintf(buf, "[%s]", p->name); + break; + case FE_PLACE_TEMP: + sprintf(buf, "[edx%+ld]", p->offset); + break; + } +} + +/* A temporary-based place needs its pointer in a register first. */ +static void load_place_base(const Frame *fr, const FeIrPlace *p, FILE *out) +{ + if (p->base != FE_PLACE_TEMP) return; + fprintf(out, " mov edx, [ebp%+ld]\n", temp_off(fr, p->index)); +} + +static void load_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) +{ + fprintf(out, " mov %s, [ebp%+ld]\n", reg, temp_off(fr, t)); +} + +static void store_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) +{ + fprintf(out, " mov [ebp%+ld], %s\n", temp_off(fr, t), reg); +} + +static const char *cmp_set(FeIrOp op, int is_unsigned) +{ + switch (op) { + case FE_IR_EQ: return "sete"; + case FE_IR_NE: return "setne"; + case FE_IR_LT: return is_unsigned ? "setb" : "setl"; + case FE_IR_LE: return is_unsigned ? "setbe" : "setle"; + case FE_IR_GT: return is_unsigned ? "seta" : "setg"; + case FE_IR_GE: return is_unsigned ? "setae" : "setge"; + default: return "sete"; + } +} + +static void emit_binary(const Frame *fr, const FeIrValue *v, FILE *out) +{ + int is_cmp = v->op >= FE_IR_EQ && v->op <= FE_IR_GE; + FeIrType t = is_cmp ? (FeIrType)v->imm : v->type; + const char *a = reg_of(t, 0); + const char *c = reg_of(t, 1); + load_temp(fr, v->a, "eax", out); + load_temp(fr, v->b, "ecx", out); + if (is_cmp) { + fprintf(out, " cmp %s, %s\n", a, c); + fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned)); + fprintf(out, " movzx eax, al\n"); + store_temp(fr, v->dest, "eax", out); + return; + } + switch (v->op) { + case FE_IR_ADD: fprintf(out, " add %s, %s\n", a, c); break; + case FE_IR_SUB: fprintf(out, " sub %s, %s\n", a, c); break; + case FE_IR_MUL: fprintf(out, " imul %s, %s\n", a, c); break; + case FE_IR_AND: fprintf(out, " and %s, %s\n", a, c); break; + case FE_IR_OR: fprintf(out, " or %s, %s\n", a, c); break; + case FE_IR_XOR: fprintf(out, " xor %s, %s\n", a, c); break; + case FE_IR_SHL: fprintf(out, " shl %s, cl\n", a); break; + case FE_IR_SHR: + fprintf(out, " %s %s, cl\n", + v->is_unsigned ? "shr" : "sar", a); + break; + case FE_IR_DIV: + case FE_IR_MOD: + /* The divide instructions use edx:eax, so the operands have to be + widened to 32 bits whatever the declared width is. */ + if (v->is_unsigned) fprintf(out, " xor edx, edx\n"); + else fprintf(out, " cdq\n"); + fprintf(out, " %s ecx\n", v->is_unsigned ? "div " : "idiv"); + if (v->op == FE_IR_MOD) fprintf(out, " mov eax, edx\n"); + break; + default: break; + } + store_temp(fr, v->dest, "eax", out); +} + +static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) +{ + char addr[128]; + unsigned i; + switch (v->op) { + case FE_IR_CONST: + fprintf(out, " mov eax, %ld\n", v->imm); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_LOAD: + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + if (v->type == FE_IR_I8) + fprintf(out, " movzx eax, byte ptr %s\n", addr); + else if (v->type == FE_IR_I16) + fprintf(out, " movzx eax, word ptr %s\n", addr); + else + fprintf(out, " mov eax, dword ptr %s\n", addr); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_STORE: + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + load_temp(fr, v->a, "eax", out); + fprintf(out, " mov %s %s, %s\n", word_of(v->type == FE_IR_VOID + ? FE_IR_I32 : v->type), addr, reg_of(v->type == FE_IR_VOID + ? FE_IR_I32 : v->type, 0)); + break; + case FE_IR_ADDR: + load_place_base(fr, &v->place, out); + place_addr(fr, &v->place, addr); + fprintf(out, " lea eax, %s\n", addr); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_CAST: + load_temp(fr, v->a, "eax", out); + /* Narrowing is free once everything is kept in a 32-bit slot; widening + has to say whether the top bits are copies of the sign. */ + if (v->type == FE_IR_I8) + fprintf(out, " %s eax, al\n", + v->is_unsigned ? "movzx" : "movsx"); + else if (v->type == FE_IR_I16) + fprintf(out, " %s eax, ax\n", + v->is_unsigned ? "movzx" : "movsx"); + store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_CALL: + /* cdecl: arguments pushed right to left, the caller pops them. */ + for (i = v->arg_count; i > 0; --i) { + load_temp(fr, v->args[i - 1], "eax", out); + fprintf(out, " push eax\n"); + } + fprintf(out, " call %s\n", v->callee); + if (v->arg_count) + fprintf(out, " add esp, %u\n", v->arg_count * 4U); + if (v->has_dest) store_temp(fr, v->dest, "eax", out); + break; + case FE_IR_COPY: { + char dst[128]; + char src[128]; + /* The source base and the destination base both want edx, so a + temporary-based place is resolved into esi or edi first. */ + if (v->place2.base == FE_PLACE_TEMP) { + load_temp(fr, v->place2.index, "esi", out); + sprintf(src, "[esi%+ld]", v->place2.offset); + } else { + place_addr(fr, &v->place2, src); + } + if (v->place.base == FE_PLACE_TEMP) { + load_temp(fr, v->place.index, "edi", out); + sprintf(dst, "[edi%+ld]", v->place.offset); + } else { + place_addr(fr, &v->place, dst); + } + fprintf(out, " lea esi, %s\n", src); + fprintf(out, " lea edi, %s\n", dst); + fprintf(out, " mov ecx, %ld\n", v->imm); + fprintf(out, " cld\n"); + fprintf(out, " rep movsb\n"); + break; + } + default: + emit_binary(fr, v, out); + break; + } +} + +static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) +{ + Frame fr; + long storage[512]; + const FeIrBlock *b; + const FeIrValue *v; + unsigned i; + long arg = 8; + if (f->is_extern || !f->first) return; + if (f->local_count > 512) return; + frame_layout(&fr, f, storage); + + fprintf(out, "\npublic %s\n", f->name); + fprintf(out, "%s proc near\n", f->name); + fprintf(out, " push ebp\n"); + fprintf(out, " mov ebp, esp\n"); + if (fr.size) fprintf(out, " sub esp, %ld\n", fr.size); + fprintf(out, " push esi\n push edi\n"); + /* Copy the incoming arguments into the frame. */ + for (i = 0; i < f->param_count; ++i) { + fprintf(out, " mov eax, [ebp+%ld]\n", arg); + fprintf(out, " mov %s [ebp%+ld], %s\n", + word_of(f->locals[i].type), storage[i], + reg_of(f->locals[i].type, 0)); + arg += 4; + } + + for (b = f->first; b; b = b->next) { + fprintf(out, "L%s_%u:\n", f->name, b->id); + for (v = b->first; v; v = v->next) emit_value(&fr, v, out); + switch (b->term) { + case FE_IR_JMP: + fprintf(out, " jmp L%s_%u\n", f->name, b->target); + break; + case FE_IR_BR: + load_temp(&fr, b->cond, "eax", out); + fprintf(out, " test eax, eax\n"); + fprintf(out, " jnz L%s_%u\n", f->name, b->target); + fprintf(out, " jmp L%s_%u\n", f->name, b->target_else); + break; + case FE_IR_RET: + if (b->has_ret_value) load_temp(&fr, b->ret_value, "eax", out); + fprintf(out, " pop edi\n pop esi\n"); + fprintf(out, " mov esp, ebp\n pop ebp\n"); + fprintf(out, " ret\n"); + break; + case FE_IR_TRAP: + fprintf(out, " push %lu\n", b->trap_line); + fprintf(out, " push offset FE_UNIT_FILE\n"); + fprintf(out, " push %u\n", (unsigned)b->trap); + fprintf(out, " call fe_trap\n"); + fprintf(out, " add esp, 12\n"); + break; + } + } + fprintf(out, "%s endp\n", f->name); + (void)m; +} + +static void emit_string(const char *s, FILE *out) +{ + int in = 0; + fputs(" db ", out); + for (; s && *s; ++s) { + unsigned char c = (unsigned char)*s; + if (c >= 32 && c < 127 && c != '\'' && c != '"') { + if (!in) { fputc('\'', out); in = 1; } + fputc(c, out); + } else { + if (in) { fputs("',", out); in = 0; } + fprintf(out, "%u,", c); + } + } + if (in) fputc('\'', out); + else fputc('0', out); + if (in) fputs(",0", out); + fputc('\n', out); +} + +void fe_x86_emit(const FeIrModule *m, FILE *out) +{ + const FeIrFunc *f; + const FeIrGlobal *g; + int any_trap = 0; + const FeIrBlock *b; + + for (f = m->funcs; f && !any_trap; f = f->next) + for (b = f->first; b; b = b->next) + if (b->term == FE_IR_TRAP) { any_trap = 1; break; } + + fputs(".386\n.model flat\n\n", out); + for (f = m->funcs; f; f = f->next) + if (f->is_extern || !f->first) + fprintf(out, "extern %s : near\n", f->name); + if (any_trap) fputs("extern fe_trap : near\n", out); + + fputs("\n_DATA segment dword public 'DATA'\n", out); + if (any_trap) { + fputs("public FE_UNIT_FILE\nFE_UNIT_FILE label byte\n", out); + emit_string(m->unit_file, out); + } + for (g = m->globals; g; g = g->next) { + fprintf(out, "public %s\n%s label byte\n", g->name, g->name); + fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL); + } + fputs("_DATA ends\n", out); + + fputs("\n_TEXT segment dword public 'CODE'\n", out); + for (f = m->funcs; f; f = f->next) emit_func(m, f, out); + /* The runtime's entry stub calls one fixed name, so point it here. */ + if (m->entry_main) + fprintf(out, "\npublic fe_main_\nfe_main_ proc near\n" + " jmp %s\nfe_main_ endp\n", m->entry_main); + fputs("\n_TEXT ends\n\nend\n", out); +} diff --git a/fec/src/x86.h b/fec/src/x86.h new file mode 100644 index 0000000..9f3229e --- /dev/null +++ b/fec/src/x86.h @@ -0,0 +1,16 @@ +#ifndef FE_X86_H +#define FE_X86_H + +#include "ir.h" + +/* IR to i386 assembly, in the syntax Open Watcom's `wasm` accepts. + + There is no register allocator. Every temporary gets a stack slot, and every + instruction loads its operands into fixed registers, computes, and stores + the result back. That is slow code and obviously correct code, and correct + comes first: a register allocator can be dropped in later without the rest + of the compiler noticing, because it only changes where a temporary lives. */ + +void fe_x86_emit(const FeIrModule *m, FILE *out); + +#endif diff --git a/tests/build.py b/tests/build.py new file mode 100644 index 0000000..363b349 --- /dev/null +++ b/tests/build.py @@ -0,0 +1,103 @@ +"""Compile a Ferro program to a Windows executable and run it. + + fec --emit-asm -> wasm -> wlink (+ the runtime, + kernel32) -> .exe + +The toolchain is the pinned Open Watcom under `.dosboxx/watcom`, hosted: the +assembler and linker there produce PE binaries as happily as they produce DOS +ones. Nothing about this step needs a virtual machine. +""" +from __future__ import annotations + +import os +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +WATCOM = ROOT / ".dosboxx" / "watcom" +RUNTIME = ROOT / "fec" / "rt" / "start.asm" + + +def _env() -> dict: + env = dict(os.environ) + env.update(WATCOM=str(WATCOM), + INCLUDE=f"{WATCOM / 'h'};{WATCOM / 'h' / 'nt'}", + PATH=f"{WATCOM / 'binnt'}{os.pathsep}{env.get('PATH', '')}") + return env + + +def _run(cmd, cwd) -> subprocess.CompletedProcess: + return subprocess.run([str(c) for c in cmd], cwd=cwd, env=_env(), + capture_output=True, text=True, timeout=120) + + +def build(fec: Path, source: Path, out_dir: Path, no_checks: bool = False): + """Returns (exe_path, log). exe_path is None when a step failed.""" + out_dir.mkdir(parents=True, exist_ok=True) + stem = source.stem + asm = out_dir / f"{stem}.asm" + log = [] + + cmd = [fec, "--emit-asm", source, "-o", asm] + if no_checks: + cmd.append("--no-checks") + step = _run(cmd, out_dir) + log.append(("fec", step.returncode, step.stdout + step.stderr)) + if step.returncode != 0 or not asm.is_file(): + return None, log + + wasm = WATCOM / "binnt" / "wasm.exe" + for src, obj in ((asm, out_dir / f"{stem}.obj"), + (RUNTIME, out_dir / "start.obj")): + step = _run([wasm, "-q", "-zq", src, f"-fo={obj}"], out_dir) + log.append(("wasm " + src.name, step.returncode, + step.stdout + step.stderr)) + if step.returncode != 0: + return None, log + + exe = out_dir / f"{stem}.exe" + step = _run([WATCOM / "binnt" / "wlink.exe", + "system", "nt", + "file", out_dir / f"{stem}.obj", + "file", out_dir / "start.obj", + "library", WATCOM / "lib386" / "nt" / "kernel32.lib", + "name", exe, + "option", "quiet"], out_dir) + log.append(("wlink", step.returncode, step.stdout + step.stderr)) + if step.returncode != 0 or not exe.is_file(): + return None, log + return exe, log + + +def run(exe: Path): + done = subprocess.run([str(exe)], capture_output=True, text=True, + timeout=30) + return done.returncode, done.stdout + done.stderr + + +def main() -> int: + if len(sys.argv) < 2: + print("usage: build.py [--no-checks]") + return 2 + source = Path(sys.argv[1]).resolve() + fec = ROOT / ".build" / "fec.exe" + if not fec.is_file(): + print("build the front end first: uv run python tests/run.py") + return 2 + exe, log = build(fec, source, ROOT / ".build" / "out", + "--no-checks" in sys.argv) + for name, code, text in log: + if code != 0 or text.strip(): + print(f"--- {name} (exit {code})") + print(text.rstrip()) + if not exe: + return 1 + code, text = run(exe) + if text: + print(text, end="") + print(f"{exe.name} exited {code}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/run.py b/tests/run.py index 7f841ba..84cd013 100644 --- a/tests/run.py +++ b/tests/run.py @@ -34,7 +34,7 @@ ROOT = Path(__file__).resolve().parent.parent FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", - "check", "resolve", "ir", "lower", "driver") + "check", "resolve", "ir", "lower", "x86", "driver") # Fixtures live here until there is a code generator to run them against. QUARANTINE = "pending-backend" From 43555b261cd1cde697597918cd3bf38f06739eb6 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:00:33 +0900 Subject: [PATCH 138/184] =?UTF-8?q?lower+backend:=20=EB=B0=B0=EC=97=B4,=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=EA=B2=80=EC=82=AC,=20=EA=B5=AC=EC=A1=B0?= =?UTF-8?q?=EC=B2=B4,=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EC=8B=A4=ED=96=89?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=8A=A4=EC=9C=84=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 인덱스는 부호 없는 비교와 트랩으로 펴진다. --no-checks 는 메시지가 아니라 비교와 분기 자체를 없앤다 -- 그게 그 플래그의 전부다. 버그 셋: - 값으로 넘긴 구조체 파라미터는 주소로 도착하는데 lowering 이 그걸 몰라서 포인터를 구조체로 읽었다. 변수마다 by_address 를 기록한다. - 덩어리 반환의 숨은 결과 인자를 지역이 아니라 임시값으로 다뤘다. - store 에 폭이 없어서 1바이트 bool 을 4바이트로 썼다. 옆 지역변수가 뭉개졌고 logic 프로그램이 틀린 답을 냈다. tests/exec.py 가 새 스위트다. run.py 는 컴파일러가 프로그램에 대해 뭐라고 하는지 보고, 이쪽은 프로그램이 실제로 무엇을 하는지 본다. 보고만 되고 방출되지 않는 경계검사는 저기서는 통과하고 여기서는 실패한다. run.py 194/194, exec.py 6/6. --- fec/src/ir.c | 5 +- fec/src/ir.h | 5 +- fec/src/lower.c | 150 +++++++++++++++++++++++++++++++++---- fec/src/x86.c | 5 +- fec/tests/exec/arith.fe | 12 +++ fec/tests/exec/array.fe | 13 ++++ fec/tests/exec/bounds.fe | 10 +++ fec/tests/exec/logic.fe | 12 +++ fec/tests/exec/loopcall.fe | 14 ++++ fec/tests/exec/structs.fe | 20 +++++ tests/exec.py | 107 ++++++++++++++++++++++++++ 11 files changed, 333 insertions(+), 20 deletions(-) create mode 100644 fec/tests/exec/arith.fe create mode 100644 fec/tests/exec/array.fe create mode 100644 fec/tests/exec/bounds.fe create mode 100644 fec/tests/exec/logic.fe create mode 100644 fec/tests/exec/loopcall.fe create mode 100644 fec/tests/exec/structs.fe create mode 100644 tests/exec.py diff --git a/fec/src/ir.c b/fec/src/ir.c index b5ebb20..959a75f 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -144,9 +144,10 @@ unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p) return result(m, b, v); } -void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned value) +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned value, + FeIrType t) { - FeIrValue *v = emit(m, b, FE_IR_STORE, FE_IR_VOID); + FeIrValue *v = emit(m, b, FE_IR_STORE, t); if (!v) return; v->place = p; v->a = value; diff --git a/fec/src/ir.h b/fec/src/ir.h index 38b5acb..edee432 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -160,7 +160,10 @@ FeIrPlace fe_ir_at_temp(unsigned temp, long offset); /* Instructions. Each returns the destination temporary where there is one. */ unsigned fe_ir_const(FeIrModule *m, FeIrBlock *b, FeIrType t, long v); unsigned fe_ir_load(FeIrModule *m, FeIrBlock *b, FeIrType t, FeIrPlace p); -void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned v); +/* `t` is how wide the write is. Without it a one-byte value would be stored + four bytes wide and take its neighbours with it. */ +void fe_ir_store(FeIrModule *m, FeIrBlock *b, FeIrPlace p, unsigned v, + FeIrType t); unsigned fe_ir_addr(FeIrModule *m, FeIrBlock *b, FeIrPlace p); unsigned fe_ir_binary(FeIrModule *m, FeIrBlock *b, FeIrOp op, FeIrType t, unsigned a, unsigned c, int is_unsigned); diff --git a/fec/src/lower.c b/fec/src/lower.c index 23926ea..4bc3d6a 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -17,6 +17,9 @@ typedef struct LowerVar { const char *cname; unsigned local; + /* An aggregate parameter arrives as an address, so the slot holds a + pointer and the value is one dereference away. */ + int by_address; } LowerVar; typedef struct Lower { @@ -45,6 +48,8 @@ typedef struct Slot { static Slot lower_expr(Lower *L, FeNode *n); static void lower_stmt(Lower *L, FeNode *n); +static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, + unsigned long size); static void fail(Lower *L, const char *why, FeNode *n) { @@ -163,20 +168,19 @@ static unsigned declare_var(Lower *L, const char *cname, const FeType *t, if (L->var_count < LOWER_MAX_LOCALS) { L->vars[L->var_count].cname = cname; L->vars[L->var_count].local = local; + L->vars[L->var_count].by_address = 0; ++L->var_count; } return local; } -static int find_var(Lower *L, const char *cname, unsigned *out) +static LowerVar *find_var(Lower *L, const char *cname) { unsigned i; if (!cname) return 0; for (i = L->var_count; i > 0; --i) - if (L->vars[i - 1].cname && strcmp(L->vars[i - 1].cname, cname) == 0) { - *out = L->vars[i - 1].local; - return 1; - } + if (L->vars[i - 1].cname && strcmp(L->vars[i - 1].cname, cname) == 0) + return &L->vars[i - 1]; return 0; } @@ -187,6 +191,50 @@ static FeIrBlock *new_block(Lower *L) return fe_ir_block(L->m, L->fn); } +/* A check that must hold. `ok` is a condition; when it is false the program + stops where it is. `--no-checks` removes the comparison and the branch, not + just the message, which is the whole point of the flag. */ +static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) +{ + FeIrBlock *bad = new_block(L); + FeIrBlock *cont = new_block(L); + fe_ir_br(L->b, ok, cont->id, bad->id); + L->b = bad; + fe_ir_trap(L->b, reason, line); + L->b = cont; +} + +/* Somewhere to build an aggregate that has no home of its own yet. */ +static unsigned scratch(Lower *L, const FeType *t, const char *why) +{ + return fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), ir_align(t), why); +} + +/* A slice is a pointer and a length, in that order. Both the compiler and the + runtime read it this way, so the offsets live here and nowhere else. */ +#define SLICE_PTR_OFFSET 0L +#define SLICE_LEN_OFFSET 4L + +/* The number of elements an indexable place holds, and where the first element + is. An array is its own storage; a slice points at someone else's. */ +static void indexable_parts(Lower *L, Slot base, const FeType *t, + unsigned *data, unsigned *length, FeNode *n) +{ + if (t && t->kind == FE_TYPE_ARRAY) { + *data = as_address(L, base, n); + *length = fe_ir_const(L->m, L->b, FE_IR_I32, (long)t->length); + return; + } + if (!base.is_place) { fail(L, "a slice with no place", n); *data = 0; *length = 0; return; } + *data = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_temp(as_address(L, base, n), SLICE_PTR_OFFSET)); + { + FeIrPlace lp = base.place; + lp.offset += SLICE_LEN_OFFSET; + *length = fe_ir_load(L->m, L->b, FE_IR_I32, lp); + } +} + /* ---------------------------------------------------------- expressions --- */ static FeIrOp binary_op(const char *op, int *is_cmp) @@ -268,12 +316,12 @@ static Slot lower_logical(Lower *L, FeNode *n, int is_and) unsigned right; L->b = entry; left = as_value(L, lower_expr(L, n->a), n->a); - fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), left); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), left, FE_IR_I8); if (is_and) fe_ir_br(L->b, left, rhs->id, join->id); else fe_ir_br(L->b, left, join->id, rhs->id); L->b = rhs; right = as_value(L, lower_expr(L, n->b), n->b); - fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), right); + fe_ir_store(L->m, L->b, fe_ir_at_local(result, 0), right, FE_IR_I8); fe_ir_jmp(L->b, join->id); L->b = join; return slot_place(fe_ir_at_local(result, 0), FE_IR_I8, 1); @@ -329,9 +377,15 @@ static Slot lower_expr(Lower *L, FeNode *n) literal_value(n)), it == FE_IR_VOID ? FE_IR_I32 : it); case FE_N_IDENT: { - unsigned local; - if (find_var(L, n->cname, &local)) - return slot_place(fe_ir_at_local(local, 0), it, ir_size(t)); + LowerVar *var = find_var(L, n->cname); + if (var) { + if (var->by_address) { + unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(var->local, 0)); + return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); + } + return slot_place(fe_ir_at_local(var->local, 0), it, ir_size(t)); + } if (n->cname) return slot_place(fe_ir_at_global(n->cname, 0), it, ir_size(t)); fail(L, "an unresolved name", n); @@ -380,6 +434,19 @@ static Slot lower_expr(Lower *L, FeNode *n) unsigned p = as_value(L, lower_expr(L, n->a), n->a); return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); } + /* `.n` is how many elements there are, which an array knows at + compile time and a slice carries beside its pointer. */ + if (n->b && n->b->text && !strcmp(n->b->text, "n")) { + FeType *bt = n->a ? n->a->sem_type : 0; + Slot base; + if (bt && bt->kind == FE_TYPE_ARRAY) + return slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, + (long)bt->length), FE_IR_I32); + base = lower_expr(L, n->a); + if (!base.is_place) { fail(L, "a length of a temporary", n); return slot_void(); } + base.place.offset += SLICE_LEN_OFFSET; + return slot_place(base.place, FE_IR_I32, 4); + } /* A field is a constant offset from the base. */ { FeType *base = n->a ? n->a->sem_type : 0; @@ -400,6 +467,58 @@ static Slot lower_expr(Lower *L, FeNode *n) b.place.offset += (long)field->offset; return slot_place(b.place, it, ir_size(t)); } + case FE_N_INDEX: { + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + Slot base; + unsigned data; + unsigned length; + unsigned index; + unsigned scale; + unsigned offset; + unsigned addr; + if (n->c || !n->b) { fail(L, "a slice expression", n); return slot_void(); } + base = lower_expr(L, n->a); + indexable_parts(L, base, bt, &data, &length, n); + index = as_value(L, lower_expr(L, n->b), n->b); + if (!L->c->no_checks) { + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, + index, length, 1); + guard(L, ok, FE_TRAP_BOUNDS, n->loc.line); + } + scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + offset = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, index, scale, 1); + addr = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, offset, 1); + return slot_place(fe_ir_at_temp(addr, 0), ir_type(elem), ir_size(elem)); + } + case FE_N_ARRAY_INIT: { + unsigned local = scratch(L, t, "array"); + FeType *elem = t ? t->elem : 0; + unsigned long step = ir_size(elem); + long at = 0; + FeNode *x; + for (x = n->children; x; x = x->next) { + Slot v = lower_expr(L, x); + store_into(L, fe_ir_at_local(local, at), v, x, step); + at += (long)step; + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + case FE_N_STRUCT_INIT: { + unsigned local = scratch(L, t, "struct"); + FeNode *f; + for (f = n->children; f; f = f->next) { + FeFieldType *field; + Slot v; + if (f->kind != FE_N_FIELD) continue; + field = fe_type_field(t, f->text); + if (!field) { fail(L, "an unresolved field", f); return slot_void(); } + v = lower_expr(L, f->a); + store_into(L, fe_ir_at_local(local, (long)field->offset), v, f, + ir_size(field->type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } case FE_N_CALL: return lower_call(L, n); case FE_N_EXPR: @@ -420,7 +539,7 @@ static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, fe_ir_copy(L->m, L->b, dst, value.place, size); return; } - fe_ir_store(L->m, L->b, dst, as_value(L, value, n)); + fe_ir_store(L->m, L->b, dst, as_value(L, value, n), value.type); } static void lower_return(Lower *L, FeNode *n) @@ -429,8 +548,9 @@ static void lower_return(Lower *L, FeNode *n) if (!n->a) { fe_ir_ret(L->b, 0, 0); return; } v = lower_expr(L, n->a); if (L->fn->returns_by_address) { - store_into(L, fe_ir_at_temp(L->ret_local, 0), v, n, - ir_size(L->ret_type)); + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + store_into(L, fe_ir_at_temp(dst, 0), v, n, ir_size(L->ret_type)); fe_ir_ret(L->b, 0, 0); return; } @@ -550,13 +670,15 @@ static void lower_fn(Lower *L, FeNode *fn) for (p = fn->a ? fn->a->children : 0; p; p = p->next) { FeType *pt = fe_type_from_ast(&L->c->types, p->a); /* An aggregate parameter arrives as an address. */ - unsigned local = ir_type(pt) == FE_IR_MEM + int by_address = ir_type(pt) == FE_IR_MEM; + unsigned local = by_address ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), p->text); if (L->var_count < LOWER_MAX_LOCALS) { L->vars[L->var_count].cname = p->cname; L->vars[L->var_count].local = local; + L->vars[L->var_count].by_address = by_address; ++L->var_count; } } diff --git a/fec/src/x86.c b/fec/src/x86.c index 11c6ef0..05348a1 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -204,9 +204,8 @@ static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) load_place_base(fr, &v->place, out); place_addr(fr, &v->place, addr); load_temp(fr, v->a, "eax", out); - fprintf(out, " mov %s %s, %s\n", word_of(v->type == FE_IR_VOID - ? FE_IR_I32 : v->type), addr, reg_of(v->type == FE_IR_VOID - ? FE_IR_I32 : v->type, 0)); + fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, + reg_of(v->type, 0)); break; case FE_IR_ADDR: load_place_base(fr, &v->place, out); diff --git a/fec/tests/exec/arith.fe b/fec/tests/exec/arith.fe new file mode 100644 index 0000000..e9cb2ff --- /dev/null +++ b/fec/tests/exec/arith.fe @@ -0,0 +1,12 @@ +// EXIT:10 +unit arith; + +fn main() -> i32 { + let a: i32 = 7; + let b: i32 = 6; + var r: i32 = a * b; + r = r - 2; + r = r / 4; + if r == 10 { return r; } + return 99; +} diff --git a/fec/tests/exec/array.fe b/fec/tests/exec/array.fe new file mode 100644 index 0000000..cf1e7ce --- /dev/null +++ b/fec/tests/exec/array.fe @@ -0,0 +1,13 @@ +// EXIT:100 +unit array; + +fn main() -> i32 { + let a: [4]i32 = [10, 20, 30, 40]; + var sum: i32 = 0; + var i: i32 = 0; + while i < 4 { + sum = sum + a[i]; + i = i + 1; + } + return sum; +} diff --git a/fec/tests/exec/bounds.fe b/fec/tests/exec/bounds.fe new file mode 100644 index 0000000..3772539 --- /dev/null +++ b/fec/tests/exec/bounds.fe @@ -0,0 +1,10 @@ +// EXIT:3 +// OUTPUT:index out of bounds +// NOCHECKS:0 +unit bounds; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let x: i32 = a[2]; + return x - x; +} diff --git a/fec/tests/exec/logic.fe b/fec/tests/exec/logic.fe new file mode 100644 index 0000000..dc65c94 --- /dev/null +++ b/fec/tests/exec/logic.fe @@ -0,0 +1,12 @@ +// EXIT:1 +unit logic; + +fn side(v: i32) -> bool { return v > 0; } + +fn main() -> i32 { + let a: bool = true and side(1); + let b: bool = false or side(2); + let c: bool = not side(0); + if a and b and c { return 1; } + return 0; +} diff --git a/fec/tests/exec/loopcall.fe b/fec/tests/exec/loopcall.fe new file mode 100644 index 0000000..f3b3bb1 --- /dev/null +++ b/fec/tests/exec/loopcall.fe @@ -0,0 +1,14 @@ +// EXIT:55 +unit loopcall; + +fn add(a: i32, b: i32) -> i32 { return a + b; } + +fn main() -> i32 { + var total: i32 = 0; + var i: i32 = 0; + while i < 10 { + total = total + add(i, 1); + i = i + 1; + } + return total; +} diff --git a/fec/tests/exec/structs.fe b/fec/tests/exec/structs.fe new file mode 100644 index 0000000..826966b --- /dev/null +++ b/fec/tests/exec/structs.fe @@ -0,0 +1,20 @@ +// EXIT:97 +unit structs; + +struct Point { x: i32, y: i32, } + +fn make(a: i32, b: i32) -> Point { return Point{ x: a, y: b }; } +fn swap(p: Point) -> Point { return Point{ x: p.y, y: p.x }; } + +fn main() -> i32 { + let p: Point = make(3, 8); + let q: Point = swap(p); + let grid: [3]Point = [make(1,1), make(2,2), make(3,3)]; + var s: i32 = 0; + var i: i32 = 0; + while i < 3 { + s = s + grid[i].x * grid[i].y; + i = i + 1; + } + return q.x * 10 + q.y + s; +} diff --git a/tests/exec.py b/tests/exec.py new file mode 100644 index 0000000..92d0ea6 --- /dev/null +++ b/tests/exec.py @@ -0,0 +1,107 @@ +"""Compile the programs under `fec/tests/exec/`, run them, and check what they do. + +A program says what it should do in its first lines: + + // EXIT:55 the process must exit with this code + // OUTPUT:hello this text must appear in what it wrote + // NOCHECKS:0 build it a second time with --no-checks and expect + this exit code instead + +The point of this suite is different from `run.py`. That one checks what the +compiler says about a program; this one checks what the program does. A bounds +check that is reported but never emitted passes there and fails here. +""" +from __future__ import annotations + +import argparse +import re +import shutil +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import build as builder # noqa: E402 + +ROOT = Path(__file__).resolve().parent.parent +PROGRAMS = ROOT / "fec" / "tests" / "exec" + + +def expectations(path: Path) -> dict: + want = {} + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line.startswith("//"): + break + m = re.match(r"//\s*(EXIT|OUTPUT|NOCHECKS):(.*)", line) + if m: + key, value = m.group(1), m.group(2).strip() + want[key] = int(value) if key in ("EXIT", "NOCHECKS") else value + return want + + +def check_one(fec: Path, path: Path, out_dir: Path) -> tuple[bool, str]: + want = expectations(path) + if "EXIT" not in want: + return False, "no // EXIT: marker" + + exe, log = builder.build(fec, path, out_dir) + if not exe: + detail = "\n".join(f" {n} (exit {c}): {t.strip()}" + for n, c, t in log if c != 0 or t.strip()) + return False, "did not build\n" + detail + code, text = builder.run(exe) + if code != want["EXIT"]: + return False, f"exited {code}, expected {want['EXIT']}\n {text.strip()}" + if "OUTPUT" in want and want["OUTPUT"] not in text: + return False, f"output has no {want['OUTPUT']!r}\n {text.strip()}" + + if "NOCHECKS" in want: + exe2, log2 = builder.build(fec, path, out_dir / "nochecks", + no_checks=True) + if not exe2: + return False, "did not build with --no-checks" + code2, _ = builder.run(exe2) + if code2 != want["NOCHECKS"]: + return False, (f"--no-checks exited {code2}, expected " + f"{want['NOCHECKS']}") + return True, "" + + +def main() -> int: + ap = argparse.ArgumentParser(description="run the compiled programs") + ap.add_argument("-k", dest="select") + ap.add_argument("-v", dest="verbose", action="store_true") + args = ap.parse_args() + + fec = ROOT / ".build" / "fec.exe" + if not fec.is_file(): + print("build the front end first: uv run python tests/run.py") + return 2 + out_dir = ROOT / ".build" / "exec" + if out_dir.exists(): + shutil.rmtree(out_dir, ignore_errors=True) + + cases = sorted(PROGRAMS.rglob("*.fe")) + if args.select: + cases = [p for p in cases if args.select in p.as_posix()] + if not cases: + print("no programs found") + return 1 + + failed = [] + for path in cases: + ok, why = check_one(fec, path, out_dir / path.stem) + rel = path.relative_to(PROGRAMS).as_posix() + if ok: + if args.verbose: + print(f" ok {rel}") + else: + failed.append((rel, why)) + for rel, why in failed: + print(f"FAIL {rel}: {why}") + print(f"\n{len(cases) - len(failed)}/{len(cases)} programs behaved") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From 096a5db4112457c60efb5b77866c295c12414ebe Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:06:35 +0900 Subject: [PATCH 139/184] =?UTF-8?q?lower:=20=EC=98=B5=EC=85=94=EB=84=90,?= =?UTF-8?q?=20=EC=97=90=EB=9F=AC=20=EC=9C=A0=EB=8B=88=EC=98=A8,=20try/catc?= =?UTF-8?q?h/orelse,=20defer,=20for?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 옵셔널은 태그와 페이로드, 에러 유니온은 오류 코드와 페이로드다. 코드 0 이 '오류 없음'이다. 페이로드 위치 규칙을 types.c 로 옮겨서 레이아웃 패스와 코드 생성기가 같은 것을 본다. try 는 분기다. 실패면 지금 함수의 에러 유니온을 그 코드로 만들어 나간다 -- 그 전에 defer 를 돌린다. catch 와 orelse 는 오른쪽을 필요할 때만 평가하므로 역시 분기다. for 는 세 형태를 공유한다: 세는 것, 원소를 도는 것, 위치까지 받는 것. 개수는 본문 전에 한 번 읽는다. 원소 바인딩은 참조다 -- 그래서 루프가 원본에 쓸 수 있다. error.Name 은 빌드 전체에서 이름을 모아 철자 순으로 1부터 번호를 준다 (SPEC 4.6). 빌드 순서가 결과를 바꾸지 않는다. run.py 197/197, exec.py 9/9. --- fec/src/lower.c | 425 ++++++++++++++++++++++++++++++++++++- fec/src/types.c | 14 ++ fec/src/types.h | 4 + fec/tests/exec/errunion.fe | 18 ++ fec/tests/exec/forloop.fe | 11 + fec/tests/exec/optional.fe | 15 ++ 6 files changed, 485 insertions(+), 2 deletions(-) create mode 100644 fec/tests/exec/errunion.fe create mode 100644 fec/tests/exec/forloop.fe create mode 100644 fec/tests/exec/optional.fe diff --git a/fec/src/lower.c b/fec/src/lower.c index 4bc3d6a..aec95a5 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -1,5 +1,6 @@ #include "lower.h" #include +#include "m7.h" #include /* ------------------------------------------------------------------------- * @@ -35,6 +36,16 @@ typedef struct Lower { unsigned break_target[32]; unsigned continue_target[32]; unsigned loop_depth; + /* `defer` blocks in the order they were written. Every exit path runs the + ones that are live, last written first. */ + FeNode *deferred[32]; + unsigned defer_count; + /* Every `error.Name` used anywhere in the build, sorted, numbered from one. + SPEC 4.6: the names are collected rather than declared, and the order is + fixed by the spelling so that the same program always gets the same + codes however the build was ordered. */ + const char *error_names[256]; + unsigned error_count; int failed; } Lower; @@ -50,6 +61,19 @@ static Slot lower_expr(Lower *L, FeNode *n); static void lower_stmt(Lower *L, FeNode *n); static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, unsigned long size); +static void lower_for(Lower *L, FeNode *n); +static Slot wrap_context(Lower *L, Slot v, FeNode *n); +static Slot lower_try(Lower *L, FeNode *n); +static Slot lower_lazy(Lower *L, FeNode *n, int is_catch); +static Slot wrapper_payload(Lower *L, Slot w, const FeType *t); +static unsigned scratch(Lower *L, const FeType *t, const char *why); +static int uses_niche(const FeType *t); +static FeIrType tag_type(const FeType *t); +static void run_deferred(Lower *L, unsigned from); +static unsigned declare_var(Lower *L, const char *cname, const FeType *t, + const char *name); +static void indexable_parts(Lower *L, Slot base, const FeType *t, + unsigned *data, unsigned *length, FeNode *n); static void fail(Lower *L, const char *why, FeNode *n) { @@ -204,6 +228,19 @@ static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) L->b = cont; } +/* A tag says which of the two things a wrapper holds. An optional is one byte + at the front unless the payload has a spare representation; an error union is + a two-byte error code, and zero means there is no error. */ +static FeIrType tag_type(const FeType *t) +{ + return t && t->kind == FE_TYPE_ERROR_UNION ? FE_IR_I16 : FE_IR_I8; +} + +static int uses_niche(const FeType *t) +{ + return t && t->kind == FE_TYPE_OPTIONAL && fe_m7_optional_uses_niche(t->elem); +} + /* Somewhere to build an aggregate that has no home of its own yet. */ static unsigned scratch(Lower *L, const FeType *t, const char *why) { @@ -235,6 +272,46 @@ static void indexable_parts(Lower *L, Slot base, const FeType *t, } } +/* ------------------------------------------------------- error codes ----- */ + +static void note_error_name(Lower *L, const char *name) +{ + unsigned i; + unsigned at; + if (!name || L->error_count >= 256) return; + for (i = 0; i < L->error_count; ++i) + if (!strcmp(L->error_names[i], name)) return; + /* Kept sorted as it is built, so the numbering is the spelling order. */ + at = L->error_count; + while (at > 0 && strcmp(L->error_names[at - 1], name) > 0) { + L->error_names[at] = L->error_names[at - 1]; + --at; + } + L->error_names[at] = name; + ++L->error_count; +} + +static void collect_error_names(Lower *L, FeNode *n) +{ + FeNode *x; + if (!n) return; + if (n->kind == FE_N_MEMBER && n->a && n->a->kind == FE_N_IDENT && + n->a->text && !strcmp(n->a->text, "error") && n->b && n->b->text) + note_error_name(L, n->b->text); + collect_error_names(L, n->a); + collect_error_names(L, n->b); + collect_error_names(L, n->c); + for (x = n->children; x; x = x->next) collect_error_names(L, x); +} + +static long error_code(Lower *L, const char *name) +{ + unsigned i; + for (i = 0; i < L->error_count; ++i) + if (!strcmp(L->error_names[i], name)) return (long)(i + 1); + return 0; +} + /* ---------------------------------------------------------- expressions --- */ static FeIrOp binary_op(const char *op, int *is_cmp) @@ -363,7 +440,19 @@ static Slot lower_call(Lower *L, FeNode *n) return slot_value(fe_ir_call(L->m, L->b, rt, callee, args, count), rt); } +static Slot lower_expr_core(Lower *L, FeNode *n); + +/* Every expression may be standing where a wrapper is expected, so the wrap is + applied once, here, rather than at each place that could need it. */ static Slot lower_expr(Lower *L, FeNode *n) +{ + Slot v; + if (!n || L->failed) return slot_void(); + v = lower_expr_core(L, n); + return n->sem_context ? wrap_context(L, v, n) : v; +} + +static Slot lower_expr_core(Lower *L, FeNode *n) { FeType *t; FeIrType it; @@ -397,6 +486,8 @@ static Slot lower_expr(Lower *L, FeNode *n) unsigned a; unsigned b; FeIrType operand; + if (n->text && !strcmp(n->text, "orelse")) return lower_lazy(L, n, 0); + if (n->text && !strcmp(n->text, "catch")) return lower_lazy(L, n, 1); if (n->text && (!strcmp(n->text, "and") || !strcmp(n->text, "or"))) return lower_logical(L, n, !strcmp(n->text, "and")); op = binary_op(n->text, &is_cmp); @@ -410,6 +501,7 @@ static Slot lower_expr(Lower *L, FeNode *n) is_cmp ? FE_IR_I8 : operand); } case FE_N_UNARY: + if (n->text && !strcmp(n->text, "try")) return lower_try(L, n); if (n->text && !strcmp(n->text, "-")) { unsigned zero = fe_ir_const(L->m, L->b, it, 0); unsigned v = as_value(L, lower_expr(L, n->a), n->a); @@ -429,6 +521,19 @@ static Slot lower_expr(Lower *L, FeNode *n) fail(L, "this unary operator", n); return slot_void(); case FE_N_MEMBER: + /* `error.Name` is a member of the open default set: a code, and + nothing to look up. */ + if (n->a && n->a->kind == FE_N_IDENT && n->a->text && + !strcmp(n->a->text, "error") && n->b && n->b->text) + return slot_value(fe_ir_const(L->m, L->b, FE_IR_I16, + error_code(L, n->b->text)), + FE_IR_I16); + /* `.?` is the payload of an optional the checker already proved is + there. */ + if (n->text && !strcmp(n->text, ".?")) { + FeType *bt = n->a ? n->a->sem_type : 0; + return wrapper_payload(L, lower_expr(L, n->a), bt); + } /* `p.^` reads through a pointer. */ if (n->text && !strcmp(n->text, ".^")) { unsigned p = as_value(L, lower_expr(L, n->a), n->a); @@ -529,6 +634,168 @@ static Slot lower_expr(Lower *L, FeNode *n) } } +/* Run the `defer` blocks that are live, most recent first. A `return` in the + middle of a function still owes them, so every exit path calls this. */ +static void run_deferred(Lower *L, unsigned from) +{ + unsigned i; + for (i = L->defer_count; i > from; --i) lower_stmt(L, L->deferred[i - 1]); +} + +/* ------------------------------------------------------- wrappers -------- * + * An optional is a tag and a payload; an error union is an error code and a + * payload, where a code of zero means there is no error. Both are memory, and + * both are built the same way: write the tag, then write the value after it. + * -------------------------------------------------------------------------- */ + +static Slot wrap_context(Lower *L, Slot v, FeNode *n) +{ + FeType *want = n->sem_context; + unsigned local; + long payload_at; + if (!want) return v; + local = scratch(L, want, "wrapped"); + payload_at = (long)fe_type_payload_offset(want); + if (want->kind == FE_TYPE_OPTIONAL) { + if (fe_m7_is_null(n)) { + /* A payload with a spare representation uses it for "nothing" + instead of carrying a separate tag. */ + unsigned z = fe_ir_const(L->m, L->b, + uses_niche(want) ? FE_IR_PTR : FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), z, + uses_niche(want) ? FE_IR_PTR : FE_IR_I8); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + if (!uses_niche(want)) { + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), one, FE_IR_I8); + } + store_into(L, fe_ir_at_local(local, payload_at), v, n, + ir_size(want->elem)); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + if (want->kind == FE_TYPE_ERROR_UNION) { + FeType *value_type = want->error_value; + if (n->sem_type && n->sem_type->is_error) { + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), + as_value(L, v, n), FE_IR_I16); + } else { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), zero, FE_IR_I16); + if (value_type && value_type->kind != FE_TYPE_VOID) + store_into(L, fe_ir_at_local(local, payload_at), v, n, + ir_size(value_type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + return v; +} + +/* The tag of a wrapper that is already in memory. */ +static unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n) +{ + FeIrPlace p; + if (!w.is_place) { fail(L, "a wrapper with no place", n); return 0; } + p = w.place; + if (uses_niche(t)) return fe_ir_load(L->m, L->b, FE_IR_PTR, p); + return fe_ir_load(L->m, L->b, tag_type(t), p); +} + +static Slot wrapper_payload(Lower *L, Slot w, const FeType *t) +{ + FeType *payload = t ? (t->kind == FE_TYPE_ERROR_UNION ? t->error_value + : t->elem) : 0; + FeIrPlace p = w.place; + (void)L; + p.offset += (long)fe_type_payload_offset(t); + return slot_place(p, ir_type(payload), ir_size(payload)); +} + +/* Leave the function with this error code, after the deferred blocks. */ +static void return_error(Lower *L, unsigned err, FeNode *n) +{ + FeType *ret = L->ret_type; + unsigned local = scratch(L, ret, "failure"); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), err, FE_IR_I16); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), fe_ir_at_local(local, 0), + ir_size(ret)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(ret), + fe_ir_at_local(local, 0)), 1); + (void)n; +} + +/* `try e` -- if e failed, leave with its error; otherwise the value. */ +static Slot lower_try(Lower *L, FeNode *n) +{ + FeType *t = n->a ? n->a->sem_type : 0; + Slot e = lower_expr(L, n->a); + unsigned err = wrapper_tag(L, e, t, n); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I16, err, zero, 1); + FeIrBlock *bad = new_block(L); + FeIrBlock *good = new_block(L); + fe_ir_br(L->b, ok, good->id, bad->id); + L->b = bad; + return_error(L, err, n); + L->b = good; + return wrapper_payload(L, e, t); +} + +/* `e orelse d` and `e catch d` both mean "the value, or that instead". The + right-hand side is only evaluated when it is needed, so it is a branch. */ +static Slot lower_lazy(Lower *L, FeNode *n, int is_catch) +{ + FeType *t = n->a ? n->a->sem_type : 0; + FeType *payload = t ? (is_catch ? t->error_value : t->elem) : 0; + Slot e; + unsigned tag; + unsigned zero; + unsigned ok; + unsigned result; + FeIrBlock *other; + FeIrBlock *join; + FeIrBlock *have; + e = lower_expr(L, n->a); + tag = wrapper_tag(L, e, t, n); + zero = fe_ir_const(L->m, L->b, is_catch || uses_niche(t) ? FE_IR_PTR + : FE_IR_I8, 0); + /* An error union is fine when its code is zero; an optional is fine when + its tag is not. */ + ok = fe_ir_binary(L->m, L->b, is_catch ? FE_IR_EQ : FE_IR_NE, + is_catch ? FE_IR_I16 : (uses_niche(t) ? FE_IR_PTR + : FE_IR_I8), + tag, zero, 1); + result = scratch(L, payload, "result"); + have = new_block(L); + other = new_block(L); + join = new_block(L); + fe_ir_br(L->b, ok, have->id, other->id); + L->b = have; + store_into(L, fe_ir_at_local(result, 0), wrapper_payload(L, e, t), n, + ir_size(payload)); + fe_ir_jmp(L->b, join->id); + L->b = other; + if (is_catch && n->c) { + /* The block form handles the error and must not fall through with a + value, so whatever it leaves behind is what the checker allowed. */ + lower_stmt(L, n->c); + } else { + Slot d = lower_expr(L, n->b); + store_into(L, fe_ir_at_local(result, 0), d, n->b, ir_size(payload)); + } + fe_ir_jmp(L->b, join->id); + L->b = join; + return slot_place(fe_ir_at_local(result, 0), ir_type(payload), + ir_size(payload)); +} + /* ----------------------------------------------------------- statements --- */ static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, @@ -545,8 +812,13 @@ static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, static void lower_return(Lower *L, FeNode *n) { Slot v; - if (!n->a) { fe_ir_ret(L->b, 0, 0); return; } + if (!n->a) { run_deferred(L, 0); fe_ir_ret(L->b, 0, 0); return; } + /* The value is computed before the deferred blocks run, because they may + destroy what it was read from. */ v = lower_expr(L, n->a); + if (v.type != FE_IR_MEM && v.is_place) + v = slot_value(as_value(L, v, n->a), v.type); + run_deferred(L, 0); if (L->fn->returns_by_address) { unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, fe_ir_at_local(L->ret_local, 0)); @@ -597,14 +869,153 @@ static void lower_while(Lower *L, FeNode *n) L->b = done; } +/* Three shapes share the keyword. + + for i in a..b { } counts + for x in thing { } walks, binding a reference to each element + for i, x in thing { } walks, binding the position as well + + The count is read once before the body, so a thing that grows underneath the + loop cannot walk past what was measured. The element binding is a reference + (`x.^` reads it), which is what lets a loop write back into the thing. */ +static void lower_for(Lower *L, FeNode *n) +{ + FeIrBlock *head; + FeIrBlock *body; + FeIrBlock *step; + FeIrBlock *done; + unsigned counter; + unsigned limit; + + if (n->c) { + /* The counting form: the variable is the count itself. */ + unsigned from = as_value(L, lower_expr(L, n->a), n->a); + unsigned to; + counter = declare_var(L, n->cname, 0, n->text); + L->fn->locals[counter].type = FE_IR_I32; + L->fn->locals[counter].size = 4; + L->fn->locals[counter].align = 4; + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), from, FE_IR_I32); + to = as_value(L, lower_expr(L, n->c), n->c); + limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "limit"); + fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), to, FE_IR_I32); + head = new_block(L); + body = new_block(L); + step = new_block(L); + done = new_block(L); + fe_ir_jmp(L->b, head->id); + L->b = head; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(limit, 0)); + unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); + fe_ir_br(L->b, more, body->id, done->id); + } + } else { + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + Slot base = lower_expr(L, n->a); + unsigned data; + unsigned length; + unsigned data_local; + unsigned item; + indexable_parts(L, base, bt, &data, &length, n); + data_local = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, "data"); + fe_ir_store(L->m, L->b, fe_ir_at_local(data_local, 0), data, FE_IR_PTR); + limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "count"); + fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), length, FE_IR_I32); + /* With two names the first is the position and the second the element; + with one it is the element. */ + counter = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "index"); + if (n->aux_cname) { + L->vars[L->var_count].cname = n->cname; + L->vars[L->var_count].local = counter; + L->vars[L->var_count].by_address = 0; + if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->aux_text); + L->vars[L->var_count].cname = n->aux_cname; + L->vars[L->var_count].local = item; + L->vars[L->var_count].by_address = 0; + if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + } else { + item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->text); + L->vars[L->var_count].cname = n->cname; + L->vars[L->var_count].local = item; + L->vars[L->var_count].by_address = 0; + if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + } + { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I32, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), zero, FE_IR_I32); + } + head = new_block(L); + body = new_block(L); + step = new_block(L); + done = new_block(L); + fe_ir_jmp(L->b, head->id); + L->b = head; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(limit, 0)); + unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); + fe_ir_br(L->b, more, body->id, done->id); + } + L->b = body; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned scale = fe_ir_const(L->m, L->b, FE_IR_I32, + (long)ir_size(elem)); + unsigned off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, i, + scale, 1); + unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(data_local, 0)); + unsigned at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, p, + off, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(item, 0), at, FE_IR_PTR); + } + L->b = head; + } + + if (L->loop_depth < 32) { + L->break_target[L->loop_depth] = done->id; + L->continue_target[L->loop_depth] = step->id; + ++L->loop_depth; + } + L->b = body; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, step->id); + L->b = step; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I32, 1); + unsigned next = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_I32, i, one, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), next, FE_IR_I32); + } + fe_ir_jmp(L->b, head->id); + if (L->loop_depth) --L->loop_depth; + L->b = done; +} + static void lower_stmt(Lower *L, FeNode *n) { FeNode *x; if (!n || L->failed) return; switch (n->kind) { - case FE_N_BLOCK: + case FE_N_BLOCK: { + unsigned outer = L->defer_count; for (x = n->children; x; x = x->next) lower_stmt(L, x); + /* Leaving a block normally runs what it deferred. An exit that jumped + away already ran them on its way out. */ + if (!L->b->terminated) run_deferred(L, outer); + L->defer_count = outer; return; + } case FE_N_LET: case FE_N_VAR: case FE_N_CONST: { @@ -644,6 +1055,12 @@ static void lower_stmt(Lower *L, FeNode *n) case FE_N_UNSAFE: lower_stmt(L, n->a); return; + case FE_N_DEFER: + if (L->defer_count < 32) L->deferred[L->defer_count++] = n->a; + return; + case FE_N_FOR: + lower_for(L, n); + return; default: fail(L, "this statement", n); return; @@ -697,6 +1114,10 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) memset(&L, 0, sizeof L); L.c = c; L.m = out; + /* The codes have to be known while the bodies are lowered, so the names + are gathered from the whole build first. */ + for (u = 0; u < c->build->count; ++u) + collect_error_names(&L, c->build->units[u].ast.root); for (u = 0; u < c->build->count; ++u) { FeUnit *unit = &c->build->units[u]; c->ast = &unit->ast; diff --git a/fec/src/types.c b/fec/src/types.c index 1762bc9..71ef161 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -417,6 +417,20 @@ unsigned long fe_type_size(const FeType *t) return t ? t->size : 0; } +unsigned long fe_type_payload_offset(const FeType *t) +{ + if (!t) return 0; + if (t->kind == FE_TYPE_ERROR_UNION) { + if (!t->error_value || t->error_value->kind == FE_TYPE_VOID) return 2; + return round_up(2UL, fe_type_align(t->error_value)); + } + if (t->kind == FE_TYPE_OPTIONAL) { + if (fe_m7_optional_uses_niche(t->elem)) return 0; + return round_up(1UL, fe_type_align(t->elem)); + } + return 0; +} + unsigned fe_type_align(const FeType *t) { return t && t->align ? t->align : 1U; diff --git a/fec/src/types.h b/fec/src/types.h index 5ade57c..4a4a4a1 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -137,6 +137,10 @@ int fe_type_is_integer(const FeType *t); int fe_type_is_indexable(const FeType *t); const char *fe_type_c_name(const FeType *t, unsigned pointer_bits); unsigned long fe_type_size(const FeType *t); +/* Where the payload of an optional or an error union sits. The tag comes + first and the value is aligned after it; both the layout pass and the + code generator have to agree, so the rule lives in one place. */ +unsigned long fe_type_payload_offset(const FeType *t); unsigned fe_type_align(const FeType *t); #endif diff --git a/fec/tests/exec/errunion.fe b/fec/tests/exec/errunion.fe new file mode 100644 index 0000000..2293507 --- /dev/null +++ b/fec/tests/exec/errunion.fe @@ -0,0 +1,18 @@ +// EXIT:9 +unit errunion; + +fn half(v: i32) -> !i32 { + if v == 0 { return error.Empty; } + return v / 2; +} + +fn chain(v: i32) -> !i32 { + let h: i32 = try half(v); + return h + 1; +} + +fn main() -> i32 { + let good: i32 = chain(16) catch 100; + let bad: i32 = chain(0) catch 0; + return good + bad; +} diff --git a/fec/tests/exec/forloop.fe b/fec/tests/exec/forloop.fe new file mode 100644 index 0000000..8eb3cc2 --- /dev/null +++ b/fec/tests/exec/forloop.fe @@ -0,0 +1,11 @@ +// EXIT:60 +unit forloop; + +fn main() -> i32 { + let a: [5]i32 = [4, 8, 12, 16, 20]; + var sum: i32 = 0; + for v in a { + sum = sum + v.^; + } + return sum; +} diff --git a/fec/tests/exec/optional.fe b/fec/tests/exec/optional.fe new file mode 100644 index 0000000..8908b84 --- /dev/null +++ b/fec/tests/exec/optional.fe @@ -0,0 +1,15 @@ +// EXIT:42 +unit optional; + +fn pick(flag: bool) -> ?i32 { + if flag { return 42; } + return null; +} + +fn main() -> i32 { + let a: ?i32 = pick(true); + let b: ?i32 = pick(false); + let x: i32 = a orelse 0; + let y: i32 = b orelse 0; + return x + y; +} From 174e6c569d50eabd39fc0d7d7ea48f1532084eb5 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:08:33 +0900 Subject: [PATCH 140/184] =?UTF-8?q?lower:=20=EC=A0=84=EC=97=AD=EA=B3=BC=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=20=EB=A6=AC=ED=84=B0=EB=9F=B4,=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B3=A0=20defer=20=EC=8B=A4=ED=96=89=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전역은 정적 저장소다. 초기값이 컴파일타임 상수면 이미지에 박고 아니면 0이다. 문자열 리터럴은 바이트를 이미지에 두고 포인터와 길이를 값으로 만든다. 같은 글자는 같은 저장소를 쓴다 -- 읽기 전용이라 공유가 공짜다. defers 프로그램이 defer 순서를 실행으로 고정한다. 등록 역순이고, 이른 return 과 끝까지 간 경로 양쪽 다 돈다. --- fec/src/ir.c | 45 +++++++++++++++++++++++++++++++++++++++ fec/src/ir.h | 7 ++++++ fec/src/lower.c | 46 +++++++++++++++++++++++++++++++++++++++- fec/src/x86.c | 12 ++++++++++- fec/tests/exec/defers.fe | 22 +++++++++++++++++++ 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 fec/tests/exec/defers.fe diff --git a/fec/src/ir.c b/fec/src/ir.c index 959a75f..0bb795c 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -1,5 +1,6 @@ #include "ir.h" #include +#include void fe_ir_module_init(FeIrModule *m) { @@ -84,6 +85,50 @@ FeIrBlock *fe_ir_block(FeIrModule *m, FeIrFunc *f) return b; } +FeIrGlobal *fe_ir_global(FeIrModule *m, const char *name, FeIrType type, + unsigned long size, unsigned align, + const unsigned char *init) +{ + FeIrGlobal *g; + for (g = m->globals; g; g = g->next) + if (!strcmp(g->name, name)) return g; + g = (FeIrGlobal *)ir_alloc(m, sizeof(FeIrGlobal)); + if (!g) return 0; + memset(g, 0, sizeof *g); + g->name = name; + g->type = type; + g->size = size; + g->align = align ? align : 1U; + g->init = init; + if (m->last_global) m->last_global->next = g; + else m->globals = g; + m->last_global = g; + return g; +} + +const char *fe_ir_string(FeIrModule *m, const char *bytes, unsigned long length) +{ + FeIrGlobal *g; + unsigned char *copy; + char *name; + unsigned serial = 0; + /* The same text twice is the same storage: string literals are read-only, + so sharing them is free. */ + for (g = m->globals; g; g = g->next) { + if (g->init && g->size == length && + !memcmp(g->init, bytes, (size_t)length)) return g->name; + ++serial; + } + copy = (unsigned char *)ir_alloc(m, length ? length : 1UL); + if (!copy) return 0; + if (length) memcpy(copy, bytes, (size_t)length); + name = (char *)ir_alloc(m, 32); + if (!name) return 0; + sprintf(name, "FE_STR_%u", serial); + g = fe_ir_global(m, name, FE_IR_MEM, length, 1, copy); + return g ? g->name : 0; +} + FeIrPlace fe_ir_at_local(unsigned index, long offset) { FeIrPlace p; diff --git a/fec/src/ir.h b/fec/src/ir.h index edee432..4db1269 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -151,6 +151,13 @@ unsigned fe_ir_local(FeIrModule *m, FeIrFunc *f, FeIrType type, unsigned long size, unsigned align, const char *name); unsigned fe_ir_temp(FeIrFunc *f); FeIrBlock *fe_ir_block(FeIrModule *m, FeIrFunc *f); +/* Static storage. `init` is `size` bytes to place there, or null for zero. */ +FeIrGlobal *fe_ir_global(FeIrModule *m, const char *name, FeIrType type, + unsigned long size, unsigned align, + const unsigned char *init); +/* A string literal's bytes, interned so the same text is stored once. */ +const char *fe_ir_string(FeIrModule *m, const char *bytes, + unsigned long length); /* Places */ FeIrPlace fe_ir_at_local(unsigned index, long offset); diff --git a/fec/src/lower.c b/fec/src/lower.c index aec95a5..e7ffc1f 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -62,6 +62,8 @@ static void lower_stmt(Lower *L, FeNode *n); static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, unsigned long size); static void lower_for(Lower *L, FeNode *n); +static void lower_global(Lower *L, FeNode *n); +static long literal_value(FeNode *n); static Slot wrap_context(Lower *L, Slot v, FeNode *n); static Slot lower_try(Lower *L, FeNode *n); static Slot lower_lazy(Lower *L, FeNode *n, int is_catch); @@ -461,6 +463,26 @@ static Slot lower_expr_core(Lower *L, FeNode *n) it = ir_type(t); switch (n->kind) { case FE_N_LITERAL: + if (n->text && n->text[0] == '"') { + /* The bytes live in the image; the value is a pointer to them and + how many there are. */ + unsigned long len = strlen(n->text); + const char *label; + unsigned local; + unsigned p; + unsigned c; + if (len >= 2) len -= 2; + label = fe_ir_string(L->m, n->text + 1, len); + if (!label) { fail(L, "a string literal", n); return slot_void(); } + local = scratch(L, t, "text"); + p = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), p, + FE_IR_PTR); + c = fe_ir_const(L->m, L->b, FE_IR_I32, (long)len); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), c, + FE_IR_I32); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } return slot_value(fe_ir_const(L->m, L->b, it == FE_IR_VOID ? FE_IR_I32 : it, literal_value(n)), @@ -1069,6 +1091,26 @@ static void lower_stmt(Lower *L, FeNode *n) /* ------------------------------------------------------------ functions --- */ +/* A global is static storage. SPEC 7.1: its initializer is evaluated at + compile time, so what reaches here is either a constant to place in the + image or nothing, and the storage starts as zeroes. */ +static void lower_global(Lower *L, FeNode *n) +{ + FeType *t = n->sem_type; + unsigned char *init = 0; + unsigned long size = ir_size(t); + if (!n->cname) return; + if (n->b && n->b->kind == FE_N_LITERAL && size && size <= 8) { + long v = literal_value(n->b); + unsigned long i; + init = (unsigned char *)fe_arena_alloc(&L->m->arena, (size_t)size); + if (init) + for (i = 0; i < size; ++i) + init[i] = (unsigned char)((v >> (i * 8)) & 0xFF); + } + fe_ir_global(L->m, n->cname, ir_type(t), size, ir_align(t), init); +} + static void lower_fn(Lower *L, FeNode *fn) { FeNode *p; @@ -1125,7 +1167,9 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) c->types.unit_name = unit->name[0] ? unit->name : "unit"; if (!out->unit_file || !out->unit_file[0]) out->unit_file = unit->path; for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) - if (n->kind == FE_N_FN && n->c) { + if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) + lower_global(&L, n); + else if (n->kind == FE_N_FN && n->c) { lower_fn(&L, n); /* The entry unit is the one the build was rooted at. */ if (u == 0 && n->text && !strcmp(n->text, "main")) diff --git a/fec/src/x86.c b/fec/src/x86.c index 05348a1..ca115de 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -368,8 +368,18 @@ void fe_x86_emit(const FeIrModule *m, FILE *out) emit_string(m->unit_file, out); } for (g = m->globals; g; g = g->next) { + unsigned long i; fprintf(out, "public %s\n%s label byte\n", g->name, g->name); - fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL); + if (!g->init) { + fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL); + continue; + } + for (i = 0; i < g->size; ++i) { + if (i % 16 == 0) fputs(" db ", out); + fprintf(out, "%u%s", g->init[i], + (i + 1 == g->size || (i % 16) == 15) ? "\n" : ","); + } + if (!g->size) fputs(" db 0\n", out); } fputs("_DATA ends\n", out); diff --git a/fec/tests/exec/defers.fe b/fec/tests/exec/defers.fe new file mode 100644 index 0000000..e9e03ab --- /dev/null +++ b/fec/tests/exec/defers.fe @@ -0,0 +1,22 @@ +// EXIT:21921 +// body(true): defer 2 then defer 1 -> 2, 21 +// body(false): note(9) first -> 219, 2192, 21921 +unit defers; + +var log: i32 = 0; + +fn note(v: i32) -> void { log = log * 10 + v; } + +fn body(early: bool) -> i32 { + defer { note(1); } + defer { note(2); } + if early { return 0; } + note(9); + return 0; +} + +fn main() -> i32 { + body(true); + body(false); + return log; +} From 4624c6d0ec8402e2230ec3fc1670c2a4dd93a01d Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:12:44 +0900 Subject: [PATCH 141/184] =?UTF-8?q?std:=20=ED=91=9C=EC=A4=80=20=EB=9D=BC?= =?UTF-8?q?=EC=9D=B4=EB=B8=8C=EB=9F=AC=EB=A6=AC=EA=B0=80=20=EC=BB=B4?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=EB=9F=AC=20=EC=98=86=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=95=B4=EC=84=9D=EB=90=98=EA=B3=A0=20=ED=98=B8=EC=B6=9C?= =?UTF-8?q?=EB=90=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std 는 예약된 이름이고 프로그램이 아니라 컴파일러와 함께 있으므로 자기 루트를 갖는다 (--std=). 본문 없는 선언은 링커가 찾을 것 -- 런타임이나 C 라이브러리 -- 이므로 IR 에 extern 으로 나간다. 런타임에 write/alloc/free/exit 를 넣었다. 이것이 표준 라이브러리가 스스로 말할 수 없는 전부이고 나머지는 Ferro 로 쓴다. @trap @unreachable @size_of @align_of @line 을 내린다. 링크 이름에서 점과 괄호를 걸렀다. 유닛 경로에는 점이 있고 제네릭 인스턴스에는 괄호가 있는데 어셈블러가 받지 않는다. --- fec/rt/start.asm | 79 +++++++++++++++++++++++++++++++++++++++++++++++ fec/src/check.c | 10 ++++++ fec/src/driver.c | 6 ++-- fec/src/lower.c | 49 +++++++++++++++++++++++++++++ fec/src/resolve.c | 19 ++++++++++-- fec/src/resolve.h | 6 +++- fec/std/core.fe | 15 ++++++--- fec/std/sys.fe | 26 ++++++++++++++-- tests/build.py | 4 ++- 9 files changed, 202 insertions(+), 12 deletions(-) diff --git a/fec/rt/start.asm b/fec/rt/start.asm index 9d6860b..340020f 100644 --- a/fec/rt/start.asm +++ b/fec/rt/start.asm @@ -122,6 +122,85 @@ reason_write: call _ExitProcess@4 fe_trap endp +; ---------------------------------------------------------------- primitives +; The standard library is written in Ferro; these are the few things it cannot +; say for itself. All cdecl. + +extern _GetProcessHeap@0 : near +extern _HeapAlloc@12 : near +extern _HeapFree@12 : near + +; fe_rt_write(handle, ptr, len) -> bytes written +public fe_rt_write +fe_rt_write proc near + push ebp + mov ebp, esp + push ebx + mov eax, [ebp+8] ; 1 = stdout, 2 = stderr + cmp eax, 2 + je pick_err + push -11 + jmp pick_done +pick_err: + push -12 +pick_done: + call _GetStdHandle@4 + push 0 + push offset written + push dword ptr [ebp+16] + push dword ptr [ebp+12] + push eax + call _WriteFile@20 + mov eax, [written] + pop ebx + mov esp, ebp + pop ebp + ret +fe_rt_write endp + +; fe_rt_alloc(n) -> pointer, or zero +public fe_rt_alloc +fe_rt_alloc proc near + push ebp + mov ebp, esp + call _GetProcessHeap@0 + push dword ptr [ebp+8] + push 8 ; HEAP_ZERO_MEMORY + push eax + call _HeapAlloc@12 + mov esp, ebp + pop ebp + ret +fe_rt_alloc endp + +; fe_rt_free(p) +public fe_rt_free +fe_rt_free proc near + push ebp + mov ebp, esp + mov eax, [ebp+8] + test eax, eax + je free_done + call _GetProcessHeap@0 + push dword ptr [ebp+8] + push 0 + push eax + call _HeapFree@12 +free_done: + mov esp, ebp + pop ebp + ret +fe_rt_free endp + +; fe_rt_exit(code) -- never returns +public fe_rt_exit +fe_rt_exit proc near + push ebp + mov ebp, esp + push dword ptr [ebp+8] + call _ExitProcess@4 +fe_rt_exit endp + public fe_start_ fe_start_ proc near call fe_main_ diff --git a/fec/src/check.c b/fec/src/check.c index a2d1dbd..134fab5 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -154,11 +154,15 @@ static FeType *node_type(FeCheck *c, FeNode *n) return t; } +/* A link-visible name. A unit path has dots in it and a generic instance has + brackets and commas, none of which an assembler will accept, so everything + outside the portable identifier set becomes an underscore. */ static char *unit_cname(FeCheck *c, const char *name) { char *u; char *p; unsigned long n; + unsigned long i; u = c->ast->root && c->ast->root->text ? c->ast->root->text : "unit"; n = (unsigned long)strlen("fe_") + (unsigned long)strlen(u) + (unsigned long)strlen(name ? name : "name") + 2UL; @@ -168,6 +172,12 @@ static char *unit_cname(FeCheck *c, const char *name) strcat(p, u); strcat(p, "_"); strcat(p, name ? name : "name"); + for (i = 0; p[i]; ++i) { + char ch = p[i]; + if (!((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || + (ch >= '0' && ch <= '9') || ch == '_')) + p[i] = '_'; + } return p; } diff --git a/fec/src/driver.c b/fec/src/driver.c index 7a15a81..2a6ce9b 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -18,7 +18,7 @@ static char *read_file(const char *name, unsigned long *size) static void usage(void) { - puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--no-checks]"); + puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--std=dir] [--no-checks]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -42,6 +42,7 @@ int main(int argc, char **argv) int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0,emit_asm=0; const char *file=0; const char *out_path=0; + const char *std_root=0; unsigned long n; char *src; FeDiags d; @@ -57,6 +58,7 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"--dump-ir")==0) dump_ir=1; else if(strcmp(argv[i],"--emit-asm")==0) emit_asm=1; else if(strcmp(argv[i],"-o")==0 && i+1text; + if (!name || name[0] != '@') return 0; + if (!strcmp(name, "@trap")) { + fe_ir_trap(L->b, FE_TRAP_EXPLICIT, n->loc.line); + L->b = new_block(L); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@unreachable")) { + fe_ir_trap(L->b, FE_TRAP_UNREACHABLE, n->loc.line); + L->b = new_block(L); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@size_of") || !strcmp(name, "@align_of")) { + FeNode *arg = n->children; + FeType *t = arg && arg->kind == FE_N_IDENT + ? fe_type_intern(&L->c->types, arg->text) : 0; + long v = !strcmp(name, "@size_of") ? (long)ir_size(t) + : (long)ir_align(t); + *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, v), FE_IR_I32); + return 1; + } + if (!strcmp(name, "@line")) { + *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, + (long)n->loc.line), FE_IR_I32); + return 1; + } + return 0; +} + static Slot lower_call(Lower *L, FeNode *n) { unsigned args[16]; @@ -417,6 +453,10 @@ static Slot lower_call(Lower *L, FeNode *n) const char *callee = n->a && n->a->cname ? n->a->cname : (n->sem_decl && n->sem_decl->cname ? n->sem_decl->cname : 0); + { + Slot built; + if (lower_builtin(L, n, &built)) return built; + } if (!callee) { fail(L, "a call with no target", n); return slot_void(); } /* An aggregate result is written through a hidden first argument. */ if (rt == FE_IR_MEM) { @@ -1169,6 +1209,15 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) lower_global(&L, n); + else if (n->kind == FE_N_FN && !n->c) { + /* A declaration with no body is something the linker will + find: the runtime, or a C library. */ + FeType *ret = n->b ? fe_type_from_ast(&c->types, n->b) : 0; + FeIrFunc *f; + if (!n->cname) continue; + f = fe_ir_func(out, n->cname, ir_type(ret), ir_size(ret)); + if (f) f->is_extern = 1; + } else if (n->kind == FE_N_FN && n->c) { lower_fn(&L, n); /* The entry unit is the one the build was rooted at. */ diff --git a/fec/src/resolve.c b/fec/src/resolve.c index 5de4c13..70baa30 100644 --- a/fec/src/resolve.c +++ b/fec/src/resolve.c @@ -190,7 +190,13 @@ static int load_unit(FeBuild *b, const char *name, FeLoc from, int have_from, unit = &b->units[b->count]; memset(unit, 0, sizeof *unit); strcpy(unit->name, name); - unit_source_path(unit->path, sizeof unit->path, b->root, name); + /* `std` is reserved (SPEC 10) and lives with the compiler, not with the + program, so it is looked up under its own root. */ + unit_source_path(unit->path, sizeof unit->path, + (name[0]=='s' && name[1]=='t' && name[2]=='d' && + (name[3]=='.' || name[3]==0) && b->std_root[0]) + ? b->std_root : b->root, + name); unit->source = read_source(unit->path, &size); if (!unit->source) { if (have_from) @@ -245,7 +251,8 @@ static int check_bindings(FeBuild *b, FeUnit *unit) return ok; } -int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags) +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags, + const char *std_root) { const char *stack[FE_BUILD_UNIT_MAX]; FeAst probe; @@ -259,6 +266,14 @@ int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags) memset(build, 0, sizeof *build); build->diags = diags; + if (std_root) { + unsigned long k = 0; + while (std_root[k] && k + 1 < sizeof build->std_root) { + build->std_root[k] = std_root[k]; + ++k; + } + build->std_root[k] = 0; + } /* The entry file fixes the import root, so it has to be parsed far enough to know its own name before anything else can be found. */ diff --git a/fec/src/resolve.h b/fec/src/resolve.h index 8116935..e859093 100644 --- a/fec/src/resolve.h +++ b/fec/src/resolve.h @@ -31,6 +31,9 @@ typedef struct FeBuild { FeUnit units[FE_BUILD_UNIT_MAX]; unsigned count; char root[260]; /* import root: where unit paths start */ + /* Where `std.*` is looked for. The standard library is not under the + program's root -- it ships with the compiler. */ + char std_root[260]; FeDiags *diags; } FeBuild; @@ -46,7 +49,8 @@ int fe_resolve_unit_identity(FeAst *ast, FeDiags *diags, const char *source_path `/a/b.fe` fixes ``, so a sibling `import c.d;` is looked for at `/c/d.fe`. Reports missing imports, import cycles, and binding conflicts. Returns non-zero when the whole graph loaded cleanly. */ -int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags); +int fe_build_load(FeBuild *build, const char *entry, FeDiags *diags, + const char *std_root); void fe_build_destroy(FeBuild *build); /* The unit a binding refers to inside `unit`, or null. diff --git a/fec/std/core.fe b/fec/std/core.fe index 38a3c58..ca49229 100644 --- a/fec/std/core.fe +++ b/fec/std/core.fe @@ -1,5 +1,12 @@ -unit core; +unit std.core; -pub error Error { Invalid = 1, Io = 2, } -pub fn panic(msg: str, file: str, line: u32) { } -pub fn assert(ok: bool) { } +// The default error set is open: `error.Name` names a member of it without +// declaring one, and the build assigns the codes (SPEC 4.6). + +pub fn assert(ok: bool) -> void { + if not ok { @trap(); } +} + +pub fn min(a: i32, b: i32) -> i32 { if a < b { return a; } return b; } +pub fn max(a: i32, b: i32) -> i32 { if a > b { return a; } return b; } +pub fn abs(v: i32) -> i32 { if v < 0 { return 0 - v; } return v; } diff --git a/fec/std/sys.fe b/fec/std/sys.fe index d9003a8..18088b8 100644 --- a/fec/std/sys.fe +++ b/fec/std/sys.fe @@ -1,2 +1,24 @@ -unit sys; -pub fn exit(code: u16); +unit std.sys; + +// The few things the language cannot say for itself. The runtime provides +// them; everything else in the standard library is written in Ferro. +extern "c" fn fe_rt_write(handle: i32, bytes: *u8, len: usize) -> i32; +extern "c" fn fe_rt_alloc(n: usize) -> *u8; +extern "c" fn fe_rt_free(p: *u8); +extern "c" fn fe_rt_exit(code: i32); + +pub fn exit(code: i32) -> void { + unsafe { fe_rt_exit(code); } +} + +pub fn raw_write(handle: i32, bytes: *u8, len: usize) -> i32 { + unsafe { return fe_rt_write(handle, bytes, len); } +} + +pub fn raw_alloc(n: usize) -> *u8 { + unsafe { return fe_rt_alloc(n); } +} + +pub fn raw_free(p: *u8) -> void { + unsafe { fe_rt_free(p); } +} diff --git a/tests/build.py b/tests/build.py index 363b349..e1eeb52 100644 --- a/tests/build.py +++ b/tests/build.py @@ -38,7 +38,9 @@ def build(fec: Path, source: Path, out_dir: Path, no_checks: bool = False): asm = out_dir / f"{stem}.asm" log = [] - cmd = [fec, "--emit-asm", source, "-o", asm] + # `std` ships with the compiler, so it is looked for beside it rather + # than beside the program. + cmd = [fec, "--emit-asm", source, "-o", asm, f"--std={ROOT / 'fec'}"] if no_checks: cmd.append("--no-checks") step = _run(cmd, out_dir) From 0de10f37b50a80925080682ce8af786b09ee80bb Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:18:16 +0900 Subject: [PATCH 142/184] =?UTF-8?q?std:=20io=20=EC=99=80=20fmt=20=EB=A5=BC?= =?UTF-8?q?=20Ferro=20=EB=A1=9C=20=EC=93=B0=EA=B3=A0,=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=90=9C=20=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8?= =?UTF-8?q?=EC=9D=B4=20=EC=B6=9C=EB=A0=A5=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit io.Writer 는 핸들 하나짜리 enum 이다. 참조도 컨텍스트 포인터도 담지 않으므로 Copy 이고 자유롭게 오간다 (SPEC 5 R8). fmt 는 sink 를 소유하지 않는다 -- 호출자가 버퍼를 주고 앞에서 몇 바이트가 쓰였는지 돌려받는다. lowering 에 추가: enum 변이 상수, match, 정수 캐스트, 문자열 이스케이프. 프론트엔드 정밀도 하나: 항상 빠져나가는 분기의 상태를 병합하지 않는다. 그 분기가 소비한 값이 그 분기를 지나지 않은 경로에서도 소비된 것처럼 보였다. fmt_i32 가 이것 때문에 못 쓰였다. extern "c" 이름은 유닛 접두사를 붙이지 않는다. 링커가 이미 아는 이름이라는 것이 그 선언의 요점이다. run.py 199/199, exec.py 11/11. --- fec/src/ast.h | 1 + fec/src/check.c | 20 +++++++++ fec/src/lower.c | 98 +++++++++++++++++++++++++++++++++++++++-- fec/src/parser.c | 4 +- fec/std/fmt.fe | 38 +++++++++++++--- fec/std/io.fe | 33 ++++++++++---- fec/tests/exec/hello.fe | 9 ++++ tests/run.py | 3 +- 8 files changed, 184 insertions(+), 22 deletions(-) create mode 100644 fec/tests/exec/hello.fe diff --git a/fec/src/ast.h b/fec/src/ast.h index e623588..2db0b06 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -43,6 +43,7 @@ struct FeNode { #define FE_NODE_SHARED 0x4U #define FE_NODE_PUB 0x8U #define FE_NODE_COMPTIME 0x10U +#define FE_NODE_EXTERN 0x20U typedef struct FeAst { FeArena arena; diff --git a/fec/src/check.c b/fec/src/check.c index 134fab5..f2e291d 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -3515,6 +3515,16 @@ static void check_stmt(FeCheckerState *s, FeNode *n) borrow_left=flow_borrow_new(s,count); flow_own_capture(left,own_left,count); flow_borrow_capture(left,borrow_left,count); + /* A branch that always leaves contributes nothing to what follows. + Merging its state would make a value it consumed look consumed + afterwards, on a path that never ran it. */ + if (m7_stmt_definitely_exits(n->b)) { + for (i=0;ic) check_stmt(s,n->c); if (n->c) { @@ -3523,6 +3533,13 @@ static void check_stmt(FeCheckerState *s, FeNode *n) borrow_right=flow_borrow_new(s,count); flow_own_capture(right,own_right,count); flow_borrow_capture(right,borrow_right,count); + if (m7_stmt_definitely_exits(n->c)) { + for (i=0;iast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_FN) { t=fe_type_intern(&c->types,""); + /* `extern "c"` means the linker already knows this name, so it is + not decorated with the unit it was declared in. */ add_symbol(s,globals,n->text,t,n,0,1, + (n->flags & FE_NODE_EXTERN) && n->text ? n->text : unit_cname(c,n->text ? n->text : "fn"),n); } return globals; diff --git a/fec/src/lower.c b/fec/src/lower.c index d5d27e8..04faf35 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -62,6 +62,8 @@ static void lower_stmt(Lower *L, FeNode *n); static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, unsigned long size); static void lower_for(Lower *L, FeNode *n); +static void lower_match(Lower *L, FeNode *n); +static int enum_has_payload(const FeType *t); static void lower_global(Lower *L, FeNode *n); static long literal_value(FeNode *n); static Slot wrap_context(Lower *L, Slot v, FeNode *n); @@ -505,14 +507,33 @@ static Slot lower_expr_core(Lower *L, FeNode *n) case FE_N_LITERAL: if (n->text && n->text[0] == '"') { /* The bytes live in the image; the value is a pointer to them and - how many there are. */ - unsigned long len = strlen(n->text); + how many there are. The lexer keeps the quotes and the escapes, + so this is where ` +` becomes one byte. */ + char text[1024]; + unsigned long raw = strlen(n->text); + unsigned long len = 0; + unsigned long i; const char *label; unsigned local; unsigned p; unsigned c; - if (len >= 2) len -= 2; - label = fe_ir_string(L->m, n->text + 1, len); + if (raw >= 2) raw -= 2; + for (i = 0; i < raw && len + 1 < sizeof text; ++i) { + char ch = n->text[1 + i]; + if (ch == 92 && i + 1 < raw) { /* a backslash */ + ++i; + switch (n->text[1 + i]) { + case 'n': ch = 10; break; + case 't': ch = 9; break; + case 'r': ch = 13; break; + case '0': ch = 0; break; + default: ch = n->text[1 + i]; break; + } + } + text[len++] = ch; + } + label = fe_ir_string(L->m, text, len); if (!label) { fail(L, "a string literal", n); return slot_void(); } local = scratch(L, t, "text"); p = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); @@ -583,6 +604,14 @@ static Slot lower_expr_core(Lower *L, FeNode *n) fail(L, "this unary operator", n); return slot_void(); case FE_N_MEMBER: + /* A payload-free variant used as a value is just its tag. */ + if (t && t->kind == FE_TYPE_ENUM && !enum_has_payload(t) && + n->b && n->b->text) { + FeVariantType *v = fe_type_variant(t, n->b->text); + if (v) + return slot_value(fe_ir_const(L->m, L->b, ir_type(t), + (long)v->tag), ir_type(t)); + } /* `error.Name` is a member of the open default set: a code, and nothing to look up. */ if (n->a && n->a->kind == FE_N_IDENT && n->a->text && @@ -688,6 +717,19 @@ static Slot lower_expr_core(Lower *L, FeNode *n) } case FE_N_CALL: return lower_call(L, n); + case FE_N_TYPE: + /* `x as T`: the operand is `a` and the target type is the node's own. + Between integers this only changes how wide the value is and whether + the top bits repeat the sign. */ + if (n->a) { + FeType *from = n->a->sem_type; + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + if (ir_type(from) == it) return slot_value(v, it); + return slot_value(fe_ir_cast(L->m, L->b, ir_type(from), it, v, + type_is_unsigned(from)), it); + } + fail(L, "this type expression", n); + return slot_void(); case FE_N_EXPR: return lower_expr(L, n->a); default: @@ -1064,6 +1106,51 @@ static void lower_for(Lower *L, FeNode *n) L->b = done; } +/* `match` over a payload-free enum or an integer: compare the tag against each + arm's pattern in turn. The checker already proved the arms cover everything, + so falling off the end cannot happen in a program that compiled -- but the + generated code has to go somewhere, and going to the join is right. */ +static void lower_match(Lower *L, FeNode *n) +{ + FeType *t = n->a ? n->a->sem_type : 0; + FeIrType it = ir_type(t); + Slot subject = lower_expr(L, n->a); + unsigned value; + FeIrBlock *join; + FeNode *arm; + if (it == FE_IR_MEM) { fail(L, "a match over a payload", n); return; } + value = as_value(L, subject, n->a); + join = new_block(L); + for (arm = n->children; arm; arm = arm->next) { + FeIrBlock *body; + FeIrBlock *next; + unsigned want; + unsigned same; + FeVariantType *v; + if (arm->kind != FE_N_ARM) continue; + if (arm->text && !strcmp(arm->text, "_")) { + lower_stmt(L, arm->a); + fe_ir_jmp(L->b, join->id); + L->b = join; + return; + } + v = t && t->kind == FE_TYPE_ENUM && arm->text + ? fe_type_variant(t, arm->text) : 0; + want = fe_ir_const(L->m, L->b, it, + v ? (long)v->tag : literal_value(arm)); + same = fe_ir_binary(L->m, L->b, FE_IR_EQ, it, value, want, 1); + body = new_block(L); + next = new_block(L); + fe_ir_br(L->b, same, body->id, next->id); + L->b = body; + lower_stmt(L, arm->a); + fe_ir_jmp(L->b, join->id); + L->b = next; + } + fe_ir_jmp(L->b, join->id); + L->b = join; +} + static void lower_stmt(Lower *L, FeNode *n) { FeNode *x; @@ -1123,6 +1210,9 @@ static void lower_stmt(Lower *L, FeNode *n) case FE_N_FOR: lower_for(L, n); return; + case FE_N_MATCH: + lower_match(L, n); + return; default: fail(L, "this statement", n); return; diff --git a/fec/src/parser.c b/fec/src/parser.c index 27d7f99..3f780bd 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -191,9 +191,9 @@ static FeNode *params(FeParser *p) static FeNode *fn_decl(FeParser *p, int pub, int external, int interrupt, int interrupt_safe) { FeToken t=p->current, name; FeNode *n; - (void)external; (void)interrupt; (void)interrupt_safe; + (void)interrupt; (void)interrupt_safe; want(p,FE_TOK_FN,"expected 'fn'"); if(!is_name(p)){error(p,"expected function name");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"fn",2);} - name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n; + name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; if(external) n->flags|=FE_NODE_EXTERN; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n; } static FeNode *field(FeParser *p, int pub) { diff --git a/fec/std/fmt.fe b/fec/std/fmt.fe index e63bc0b..dabea45 100644 --- a/fec/std/fmt.fe +++ b/fec/std/fmt.fe @@ -1,6 +1,32 @@ -unit fmt; -pub fn fmt_int_i32(buf: []mut u8, v: i32) -> str; -pub fn fmt_hex_i32(buf: []mut u8, v: i32) -> str; -pub fn fmt_char(buf: []mut u8, v: char) -> str; -pub fn fmt_bool(buf: []mut u8, v: bool) -> str; -pub fn fmt_error(buf: []mut u8, v: core.Error) -> str; +unit std.fmt; + +// Pure conversion. Nothing here owns a sink: the caller supplies the buffer +// and is told how many bytes at the front of it were written (SPEC 10). + +pub fn fmt_u32(buf: []mut u8, v: u32) -> usize { + var tmp: [10]u8 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + var value: u32 = v; + var count: usize = 0; + while true { + tmp[count] = ((value % 10) as u8) + ('0' as u8); + count = count + 1; + value = value / 10; + if value == 0 { break; } + if count == 10 { break; } + } + var i: usize = 0; + while i < count { + if i < buf.n { buf[i] = tmp[count - 1 - i]; } + i = i + 1; + } + return count; +} + +pub fn fmt_i32(buf: []mut u8, v: i32) -> usize { + if v >= 0 { return fmt_u32(buf, v as u32); } + if buf.n == 0 { return 0; } + buf[0] = '-' as u8; + var rest: []mut u8 = buf[1..buf.n]; + let digits: usize = fmt_u32(rest, (0 - v) as u32); + return digits + 1; +} diff --git a/fec/std/io.fe b/fec/std/io.fe index d7cc685..475b1d8 100644 --- a/fec/std/io.fe +++ b/fec/std/io.fe @@ -1,10 +1,25 @@ -unit io; -pub enum Writer { Stdout, Stderr, File(u16), Null } -pub enum Reader { Stdin, File(u16) } -pub fn write(w: Writer, bytes: []u8) -> !usize; -pub fn read(r: Reader, bytes: []mut u8) -> !usize; -pub struct File { - handle: u16, - pub fn close(self: Self) -> !void; - pub fn drop(self: &mut Self) { } +unit std.io; +import std.sys; + +// A writer is a handle and nothing else: an integer the runtime understands. +// It stores no reference and no context pointer, so it is Copy and can be +// passed and returned freely (SPEC 5 R8). +pub enum Writer { Null, Stdout, Stderr } + +pub fn write(w: Writer, bytes: []u8) -> usize { + if w == Writer.Null { return bytes.n; } + var handle: i32 = 1; + if w == Writer.Stderr { handle = 2; } + let done: i32 = sys.raw_write(handle, &bytes[0], bytes.n); + if done < 0 { return 0; } + return done as usize; +} + +pub fn print(bytes: []u8) -> usize { + return write(Writer.Stdout, bytes); +} + +pub fn println(bytes: []u8) -> usize { + let n: usize = write(Writer.Stdout, bytes); + return n + write(Writer.Stdout, "\n"); } diff --git a/fec/tests/exec/hello.fe b/fec/tests/exec/hello.fe new file mode 100644 index 0000000..e39fcb6 --- /dev/null +++ b/fec/tests/exec/hello.fe @@ -0,0 +1,9 @@ +// EXIT:0 +// OUTPUT:hello from ferro +unit hello; +import std.io; + +fn main() -> i32 { + io.println("hello from ferro"); + return 0; +} diff --git a/tests/run.py b/tests/run.py index 84cd013..f4d3575 100644 --- a/tests/run.py +++ b/tests/run.py @@ -80,7 +80,8 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]: want = expectation(path) # The grammar fixtures are not all well-typed; stop after parsing. mode = "--dump-ast" if path.parent.name == "parse" else "--check" - done = subprocess.run([str(fec), mode, str(path)], + done = subprocess.run([str(fec), mode, str(path), + f"--std={ROOT / 'fec'}"], capture_output=True, text=True, timeout=30) output = (done.stdout + done.stderr).strip() rejected = done.returncode != 0 From eba6f63530591ffea16e37b2e0ab1cb715dfd0a7 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:20:17 +0900 Subject: [PATCH 143/184] =?UTF-8?q?lower:=20=EC=8A=AC=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=8B=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit x[a..b] 는 인덱스가 아니라 포인터와 길이를 만든다. 양쪽 끝을 -- 서로에 대해, 그리고 있는 것에 대해 -- 검사한 뒤에 포인터를 만든다. 유효한 범위의 빈 슬라이스는 괜찮고 끝보다 늦게 시작하는 것은 아니다. 파서가 x[a] 와 x[a..] 를 구분하지 못했다. 둘 다 b 만 있고 c 가 없어서 모양이 같았다. '..' 가 있었으면 노드에 표시한다. 정수 출력이 된다: fizz 12345 -678 --- fec/src/ast.h | 3 +++ fec/src/lower.c | 47 +++++++++++++++++++++++++++++++++++++- fec/src/parser.c | 4 ++-- fec/tests/exec/printnum.fe | 20 ++++++++++++++++ 4 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 fec/tests/exec/printnum.fe diff --git a/fec/src/ast.h b/fec/src/ast.h index 2db0b06..b53221c 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -44,6 +44,9 @@ struct FeNode { #define FE_NODE_PUB 0x8U #define FE_NODE_COMPTIME 0x10U #define FE_NODE_EXTERN 0x20U +/* An index expression that had `..` in it, so it makes a slice rather than + reaching an element. `x[a]` and `x[a..]` are otherwise the same shape. */ +#define FE_NODE_SLICE 0x40U typedef struct FeAst { FeArena arena; diff --git a/fec/src/lower.c b/fec/src/lower.c index 04faf35..cf9ff73 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -62,6 +62,8 @@ static void lower_stmt(Lower *L, FeNode *n); static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, unsigned long size); static void lower_for(Lower *L, FeNode *n); +static Slot lower_slice(Lower *L, FeNode *n); +static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line); static void lower_match(Lower *L, FeNode *n); static int enum_has_payload(const FeType *t); static void lower_global(Lower *L, FeNode *n); @@ -673,7 +675,7 @@ static Slot lower_expr_core(Lower *L, FeNode *n) unsigned scale; unsigned offset; unsigned addr; - if (n->c || !n->b) { fail(L, "a slice expression", n); return slot_void(); } + if (n->flags & FE_NODE_SLICE) return lower_slice(L, n); base = lower_expr(L, n->a); indexable_parts(L, base, bt, &data, &length, n); index = as_value(L, lower_expr(L, n->b), n->b); @@ -973,6 +975,49 @@ static void lower_while(Lower *L, FeNode *n) L->b = done; } +/* `x[a..b]` makes a pointer and a length out of part of something indexable. + Both ends are checked -- against each other and against what is there -- + before the pointer is formed. An empty slice of a valid range is fine; one + that starts past its end is not. */ +static Slot lower_slice(Lower *L, FeNode *n) +{ + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + FeType *t = n->sem_type; + Slot base = lower_expr(L, n->a); + unsigned data; + unsigned length; + unsigned from; + unsigned to; + unsigned local; + unsigned scale; + unsigned off; + unsigned at; + unsigned count; + indexable_parts(L, base, bt, &data, &length, n); + from = n->b ? as_value(L, lower_expr(L, n->b), n->b) + : fe_ir_const(L->m, L->b, FE_IR_I32, 0); + to = n->c ? as_value(L, lower_expr(L, n->c), n->c) : length; + if (!L->c->no_checks) { + unsigned ordered = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, + from, to, 1); + unsigned within; + guard(L, ordered, FE_TRAP_BOUNDS, n->loc.line); + within = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, to, length, 1); + guard(L, within, FE_TRAP_BOUNDS, n->loc.line); + } + scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, from, scale, 1); + at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, off, 1); + count = fe_ir_binary(L->m, L->b, FE_IR_SUB, FE_IR_I32, to, from, 1); + local = scratch(L, t, "slice"); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), at, + FE_IR_PTR); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), count, + FE_IR_I32); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); +} + /* Three shapes share the keyword. for i in a..b { } counts diff --git a/fec/src/parser.c b/fec/src/parser.c index 3f780bd..430b31e 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -124,8 +124,8 @@ static FeNode *postfix(FeParser *p) if(eat(p,FE_TOK_LPAREN)) { m=toknode(p,FE_N_CALL,t); m->a=n; while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(m,delimited_expr(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' after call"); n=m; } else if(eat(p,FE_TOK_LBRACKET)) { m=toknode(p,FE_N_INDEX,t);m->a=n; - if(is(p,FE_TOK_DOTDOT)) m->b=0; else m->b=delimited_expr(p); - if(eat(p,FE_TOK_DOTDOT)) { if(!is(p,FE_TOK_RBRACKET)) m->c=delimited_expr(p); } + if(is(p,FE_TOK_DOTDOT)) { m->b=0; m->flags|=FE_NODE_SLICE; } else m->b=delimited_expr(p); + if(eat(p,FE_TOK_DOTDOT)) { m->flags|=FE_NODE_SLICE; if(!is(p,FE_TOK_RBRACKET)) m->c=delimited_expr(p); } want(p,FE_TOK_RBRACKET,"expected ']' after index");n=m; } else if(eat(p,FE_TOK_DOT)) { diff --git a/fec/tests/exec/printnum.fe b/fec/tests/exec/printnum.fe new file mode 100644 index 0000000..8e94185 --- /dev/null +++ b/fec/tests/exec/printnum.fe @@ -0,0 +1,20 @@ +// EXIT:0 +// OUTPUT:fizz 12345 -678 +unit printnum; +import std.io; +import std.fmt; + +fn show(v: i32) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], v); + io.print(" "); + io.print(buf[0..n]); +} + +fn main() -> i32 { + io.print("fizz"); + show(12345); + show(0 - 678); + io.print("\n"); + return 0; +} From 231c7d564b5b847a2a15592ddd77e8e882213b50 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:26:20 +0900 Subject: [PATCH 144/184] =?UTF-8?q?lower:=20=EC=A0=9C=EB=84=A4=EB=A6=AD=20?= =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=EC=99=80=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 모노모피제이션이 실제로 코드를 만드는 자리가 여기다. 프론트엔드는 어떤 인스턴스가 존재하는지만 정했다. 검사기가 인스턴스마다 선언·바인딩·유닛·링크 이름을 기록하고, lowering 이 그 바인딩을 다시 걸고 같은 본문을 자기 이름으로 내린다. 제네릭 선언 자체는 코드가 없다. comptime 인자는 값이 아니므로 호출에서 넘기지 않고 파라미터 자리도 잡지 않는다. 메서드 호출은 도달한 대상을 첫 인자로 넘긴다 -- self: Self 든 self: &Self 든 수신자의 주소로 같다. 구조체 메서드가 아예 lowering 되지 않고 있었다. exec.py 13/13. --- fec/src/check.c | 56 +++++++++++++++++++----- fec/src/check.h | 9 ++++ fec/src/lower.c | 90 +++++++++++++++++++++++++++++++++++---- fec/tests/exec/generic.fe | 21 +++++++++ 4 files changed, 157 insertions(+), 19 deletions(-) create mode 100644 fec/tests/exec/generic.fe diff --git a/fec/src/check.c b/fec/src/check.c index f2e291d..3e014db 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -464,12 +464,14 @@ static void pop_bindings(FeCheck *c, const FeBindSave *save); static void bind_self(FeCheck *c, FeType *owner); static void instance_key(char *out, const char *unit, const char *name, FeType **args, unsigned count); -static int instance_record(FeCheck *c, const char *key, FeLoc loc); +static int instance_record(FeCheck *c, const char *key, FeLoc loc, + FeNode *decl, FeUnit *home, FeType *owner); +static const char *instance_cname(FeCheck *c, const char *key); static int instance_descend(FeCheck *c, FeLoc loc); static void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, FeType *owner, FeBindSave *bindings, FeLoc site); static void check_instance_method(FeCheckerState *s, FeType *owner, - FeNode *method, FeLoc site); + FeNode *method, FeLoc site, FeNode *call); typedef struct FeFlowSlot { FeSym *sym; @@ -1318,7 +1320,7 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) fe_type_intern(&c->types,"void"); if (bound) { pop_bindings(c,&msave); - check_instance_method(s,et,method,n->loc); + check_instance_method(s,et,method,n->loc,n); } return n->sem_type; } @@ -2249,6 +2251,14 @@ static void instance_key(char *out, const char *unit, const char *name, /* Already built, or being built right now. Re-asking for a pending instance is how a recursive generic terminates, so it must not look like a new one. */ +static const char *instance_cname(FeCheck *c, const char *key) +{ + unsigned i; + for (i=0;iinstance_count;++i) + if (!strcmp(c->instances[i].key,key)) return c->instances[i].cname; + return 0; +} + static int instance_known(FeCheck *c, const char *key) { unsigned i; @@ -2257,14 +2267,27 @@ static int instance_known(FeCheck *c, const char *key) return 0; } -static int instance_record(FeCheck *c, const char *key, FeLoc loc) +static int instance_record(FeCheck *c, const char *key, FeLoc loc, + FeNode *decl, FeUnit *home, FeType *owner) { + FeInstance *inst; + unsigned i; if (instance_known(c,key)) return 0; if (c->instance_count>=FE_GENERIC_INSTANCE_MAX) { err(c,loc,"too many generic instances"); return -1; } - strcpy(c->instances[c->instance_count].key,key); + inst=&c->instances[c->instance_count]; + strcpy(inst->key,key); + inst->decl=decl; + inst->home=home ? home->name : 0; + inst->owner=owner; + inst->cname=unit_cname(c,key); + /* The bindings in force right now are the ones this instance was built + with, and lowering has to see exactly those again. */ + inst->bind_count=c->types.param_count; + for (i=0;itypes.param_count && ibinds[i]=c->types.params[i]; ++c->instance_count; return 1; } @@ -2357,7 +2380,7 @@ static FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, return unknown(c); } instance_key(key,home->name,name,args,count); - if (instance_record(c,key,loc)<0) return unknown(c); + if (instance_record(c,key,loc,decl,home,0)<0) return unknown(c); return build_struct_instance(c,home,decl,key,args,count); } @@ -2537,8 +2560,10 @@ static FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, instance_key(key,home->name,decl->text,args,want); push_bindings(c,&save,decl,args,want); result=check_call_args(s,n,sym,home->name,want); + fresh=instance_record(c,key,n->loc,decl,home,0); pop_bindings(c,&save); - fresh=instance_record(c,key,n->loc); + /* The call goes to this instance, not to the declaration it came from. */ + if (n->a) n->a->cname=(char *)instance_cname(c,key); if (fresh>0) { if (!instance_descend(c,n->loc)) return result; push_bindings(c,&save,decl,args,want); @@ -2572,8 +2597,9 @@ static FeType *check_static_method_call(FeCheckerState *s, FeNode *n, push_instance_bindings(c,&save,owner); bind_self(c,owner); result=check_call_args(s,n,&fake,home->name,0); + fresh=instance_record(c,key,n->loc,method,home,owner); pop_bindings(c,&save); - fresh=instance_record(c,key,n->loc); + if (n->a) n->a->cname=(char *)instance_cname(c,key); if (fresh>0) { if (!instance_descend(c,n->loc)) return result; push_instance_bindings(c,&save,owner); @@ -2587,7 +2613,7 @@ static FeType *check_static_method_call(FeCheckerState *s, FeNode *n, /* The body of a method on a generic instance, checked once per instance. */ static void check_instance_method(FeCheckerState *s, FeType *owner, - FeNode *method, FeLoc site) + FeNode *method, FeLoc site, FeNode *call) { FeCheck *c=s->c; FeUnit *home=current_unit(c); @@ -2596,7 +2622,17 @@ static void check_instance_method(FeCheckerState *s, FeType *owner, FeType *self_args[1]; self_args[0]=owner; instance_key(key,home->name,method->text,self_args,1); - if (instance_record(c,key,site)<=0) return; + { + FeBindSave probe; + int fresh; + push_instance_bindings(c,&probe,owner); + bind_self(c,owner); + fresh=instance_record(c,key,site,method,home,owner); + pop_bindings(c,&probe); + /* The call names this instance's copy of the method. */ + if (call && call->a) call->a->cname=(char *)instance_cname(c,key); + if (fresh<=0) return; + } if (!instance_descend(c,site)) return; push_instance_bindings(c,&save,owner); bind_self(c,owner); diff --git a/fec/src/check.h b/fec/src/check.h index 2dda600..59bde60 100644 --- a/fec/src/check.h +++ b/fec/src/check.h @@ -14,6 +14,15 @@ typedef struct FeScope FeScope; #define FE_GENERIC_INSTANCE_MAX 512 typedef struct FeInstance { char key[FE_GENERIC_KEY_MAX]; + /* What lowering needs to build this instance's code: the declaration, the + arguments bound while it was checked, the unit those names belong to, + and the name the linker will see. */ + FeNode *decl; + FeTypeBind binds[FE_TYPE_PARAM_MAX]; + unsigned bind_count; + const char *home; + const char *cname; + FeType *owner; /* set when the instance is a method */ } FeInstance; /* The checker spans a whole build, not one file. Names cross unit boundaries, diff --git a/fec/src/lower.c b/fec/src/lower.c index cf9ff73..e344e82 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -62,6 +62,8 @@ static void lower_stmt(Lower *L, FeNode *n); static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, unsigned long size); static void lower_for(Lower *L, FeNode *n); +static int fn_is_generic(const FeNode *fn); +static void lower_fn_as(Lower *L, FeNode *fn, const char *name); static Slot lower_slice(Lower *L, FeNode *n); static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line); static void lower_match(Lower *L, FeNode *n); @@ -450,7 +452,7 @@ static Slot lower_call(Lower *L, FeNode *n) { unsigned args[16]; unsigned count = 0; - FeNode *arg; + FeNode *arg = n->children; FeType *ret = n->sem_type; FeIrType rt = ir_type(ret); unsigned result_local = 0; @@ -468,7 +470,28 @@ static Slot lower_call(Lower *L, FeNode *n) ir_align(ret), "result"); args[count++] = fe_ir_addr(L->m, L->b, fe_ir_at_local(result_local, 0)); } - for (arg = n->children; arg; arg = arg->next) { + /* A method call passes what it was reached through as its first argument. + `self: Self` and `self: &Self` are the same thing here: the address of + the receiver, because an aggregate never travels in a register. */ + if (n->a && n->a->kind == FE_N_MEMBER && n->sem_decl) { + FeNode *first = n->sem_decl->a ? n->sem_decl->a->children : 0; + if (first && first->text && !strcmp(first->text, "self")) { + Slot recv = lower_expr(L, n->a->a); + args[count++] = recv.is_place ? as_address(L, recv, n->a->a) + : recv.temp; + } + } + /* A generic call passes its type arguments first. They were consumed when + the instance was chosen and carry no value, so they are not passed. */ + { + FeNode *p; + for (p = n->sem_decl && n->sem_decl->a ? n->sem_decl->a->children : 0; + p && arg; p = p->next) { + if (!(p->flags & FE_NODE_COMPTIME)) break; + arg = arg->next; + } + } + for (; arg; arg = arg->next) { Slot a = lower_expr(L, arg); if (count >= 16) { fail(L, "too many arguments", n); break; } args[count++] = a.type == FE_IR_MEM ? as_address(L, a, arg) @@ -1286,13 +1309,22 @@ static void lower_global(Lower *L, FeNode *n) fe_ir_global(L->m, n->cname, ir_type(t), size, ir_align(t), init); } -static void lower_fn(Lower *L, FeNode *fn) +static int fn_is_generic(const FeNode *fn) +{ + FeNode *p; + if (!fn) return 0; + for (p = fn->a ? fn->a->children : 0; p; p = p->next) + if (p->flags & FE_NODE_COMPTIME) return 1; + return 0; +} + +static void lower_fn_as(Lower *L, FeNode *fn, const char *name) { FeNode *p; FeType *ret = fn->b ? fe_type_from_ast(&L->c->types, fn->b) : 0; FeIrFunc *f; - if (!fn->cname) return; - f = fe_ir_func(L->m, fn->cname, ir_type(ret), ir_size(ret)); + if (!name) return; + f = fe_ir_func(L->m, name, ir_type(ret), ir_size(ret)); if (!f) return; L->fn = f; L->ret_type = ret; @@ -1302,10 +1334,16 @@ static void lower_fn(Lower *L, FeNode *fn) if (f->returns_by_address) L->ret_local = fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, "result"); for (p = fn->a ? fn->a->children : 0; p; p = p->next) { - FeType *pt = fe_type_from_ast(&L->c->types, p->a); + FeType *pt; + int by_address; + unsigned local; + /* A comptime parameter was consumed at compile time; it has no + storage and takes no argument slot. */ + if (p->flags & FE_NODE_COMPTIME) continue; + pt = fe_type_from_ast(&L->c->types, p->a); /* An aggregate parameter arrives as an address. */ - int by_address = ir_type(pt) == FE_IR_MEM; - unsigned local = by_address + by_address = ir_type(pt) == FE_IR_MEM; + local = by_address ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), p->text); @@ -1323,6 +1361,11 @@ static void lower_fn(Lower *L, FeNode *fn) fe_ir_ret(L->b, 0, 0); } +static void lower_fn(Lower *L, FeNode *fn) +{ + lower_fn_as(L, fn, fn->cname); +} + int fe_lower_program(FeCheck *c, FeIrModule *out) { Lower L; @@ -1353,12 +1396,41 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) f = fe_ir_func(out, n->cname, ir_type(ret), ir_size(ret)); if (f) f->is_extern = 1; } - else if (n->kind == FE_N_FN && n->c) { + else if (n->kind == FE_N_FN && n->c && !fn_is_generic(n)) { lower_fn(&L, n); /* The entry unit is the one the build was rooted at. */ if (u == 0 && n->text && !strcmp(n->text, "main")) out->entry_main = n->cname; } } + /* Each instance the checker reached is a function of its own: the same + body, read with different types bound, under its own link name. This is + where monomorphisation actually produces code -- the front end only + decided which instances exist. */ + for (u = 0; u < c->instance_count && !L.failed; ++u) { + FeInstance *inst = &c->instances[u]; + FeUnit *home; + FeTypeBind save[FE_TYPE_PARAM_MAX]; + unsigned save_count; + unsigned k; + if (!inst->decl || !inst->decl->c || !inst->cname || !inst->home) + continue; + home = 0; + for (k = 0; k < c->build->count; ++k) + if (!strcmp(c->build->units[k].name, inst->home)) + home = &c->build->units[k]; + if (!home) continue; + c->ast = &home->ast; + c->unit = home; + c->types.unit_name = home->name; + save_count = c->types.param_count; + for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) save[k] = c->types.params[k]; + c->types.param_count = inst->bind_count; + for (k = 0; k < inst->bind_count && k < FE_TYPE_PARAM_MAX; ++k) + c->types.params[k] = inst->binds[k]; + lower_fn_as(&L, inst->decl, inst->cname); + c->types.param_count = save_count; + for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) c->types.params[k] = save[k]; + } return !L.failed; } diff --git a/fec/tests/exec/generic.fe b/fec/tests/exec/generic.fe new file mode 100644 index 0000000..a817005 --- /dev/null +++ b/fec/tests/exec/generic.fe @@ -0,0 +1,21 @@ +// EXIT:39 +unit generic; + +struct Box(T) { + value: T, + + pub fn new(v: T) -> Self { return Self{ value: v }; } + pub fn get(self: &Self) -> T { return self.value; } +} + +fn id(comptime T: type, v: T) -> T { return v; } + +fn twice(comptime T: type, v: T) -> T { return v + v; } + +fn main() -> i32 { + let a: i32 = id(i32, 7); + let b: u8 = id(u8, 9 as u8); + let c: i32 = twice(i32, 10); + let box: Box(i32) = Box(i32).new(3); + return a + (b as i32) + c + box.get(); +} From 9510f126438240bbfdc9501ce8c9e69d838fde32 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:29:18 +0900 Subject: [PATCH 145/184] lower: mem.create / alloc_slice / destroy / replace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 할당하는 내장 함수들이다. 평범한 호출이 아니라서 여기서 편다. create 는 값을 받아 그 복사본을 가리키는 소유 포인터를 주고, 할당이 실패할 수 있으므로 결과가 에러 유니온이다. 실패 코드는 OutOfMemory 이고, 소스 어디에도 그 이름이 적혀 있지 않지만 다른 이름과 같은 표에 들어간다. 갓 할당한 저장소는 통째로 소유하므로 쓸 수 있다 -- 방해할 사람이 없다. 그래서 alloc_slice 는 ^[]mut T 를 준다. 소유 슬라이스는 포인터와 길이가 값 자체라서 .^ 로 통과할 것이 없고, destroy 는 그 안의 포인터를 푼다. heap 프로그램이 할당·try·defer 해제·for 순회를 한꺼번에 돈다: sum 4950 --- fec/src/check.c | 4 +- fec/src/lower.c | 179 ++++++++++++++++++++++++++++++++++++++++- fec/tests/exec/heap.fe | 30 +++++++ 3 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 fec/tests/exec/heap.fe diff --git a/fec/src/check.c b/fec/src/check.c index 3e014db..a86cd34 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -1198,7 +1198,9 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) b=count ? check_expr(s,count) : unknown(c); if(known(b) && !fe_type_is_integer(b)) err(c,count->loc,"slice length must be an integer"); - a=fe_type_owned(&c->types,fe_type_slice(&c->types,item)); + /* Freshly allocated storage is owned outright, so it is + writable: there is nobody else to disturb. */ + a=fe_type_owned(&c->types,fe_type_mut_slice(&c->types,item)); n->sem_type=fe_type_error_union(&c->types,a); return n->sem_type; } diff --git a/fec/src/lower.c b/fec/src/lower.c index e344e82..ae90687 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -62,6 +62,8 @@ static void lower_stmt(Lower *L, FeNode *n); static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, unsigned long size); static void lower_for(Lower *L, FeNode *n); +static int lower_mem(Lower *L, FeNode *n, Slot *out); +static long error_code(Lower *L, const char *name); static int fn_is_generic(const FeNode *fn); static void lower_fn_as(Lower *L, FeNode *fn, const char *name); static Slot lower_slice(Lower *L, FeNode *n); @@ -303,6 +305,14 @@ static void collect_error_names(Lower *L, FeNode *n) { FeNode *x; if (!n) return; + /* Allocation reports failure with a name like any other, so it has to be + in the table even though no source line writes it. */ + if (n->kind == FE_N_CALL && n->a && n->a->kind == FE_N_MEMBER && + n->a->a && n->a->a->kind == FE_N_IDENT && n->a->a->text && + !strcmp(n->a->a->text, "mem") && n->a->b && n->a->b->text && + (!strcmp(n->a->b->text, "create") || + !strcmp(n->a->b->text, "alloc_slice"))) + note_error_name(L, "OutOfMemory"); if (n->kind == FE_N_MEMBER && n->a && n->a->kind == FE_N_IDENT && n->a->text && !strcmp(n->a->text, "error") && n->b && n->b->text) note_error_name(L, n->b->text); @@ -448,6 +458,164 @@ static int lower_builtin(Lower *L, FeNode *n, Slot *out) return 0; } +/* ------------------------------------------------------------- mem.* ----- * + * The allocating intrinsics. They are not ordinary calls: `mem.create` takes a + * value and gives back an owned pointer to a copy of it, and the result is an + * error union because the allocation can fail. The runtime does the allocating; + * everything else about the shape is decided here. + * -------------------------------------------------------------------------- */ + +static const char *RT_ALLOC = "fe_rt_alloc"; +static const char *RT_FREE = "fe_rt_free"; + +static int is_mem_call(const FeNode *n, const char *what) +{ + return n && n->a && n->a->kind == FE_N_MEMBER && + n->a->a && n->a->a->kind == FE_N_IDENT && n->a->a->text && + !strcmp(n->a->a->text, "mem") && + n->a->b && n->a->b->text && !strcmp(n->a->b->text, what); +} + +/* Build `!^T`: zero and the pointer when the allocation worked, the + out-of-memory code when it did not. */ +static Slot allocation_result(Lower *L, FeNode *n, unsigned pointer) +{ + FeType *t = n->sem_type; + unsigned local = scratch(L, t, "allocated"); + long payload_at = (long)fe_type_payload_offset(t); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_PTR, 0); + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_NE, FE_IR_PTR, pointer, zero, 1); + FeIrBlock *good = new_block(L); + FeIrBlock *bad = new_block(L); + FeIrBlock *join = new_block(L); + fe_ir_br(L->b, ok, good->id, bad->id); + L->b = good; + { + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, payload_at), pointer, + FE_IR_PTR); + } + fe_ir_jmp(L->b, join->id); + L->b = bad; + { + unsigned code = fe_ir_const(L->m, L->b, FE_IR_I16, + error_code(L, "OutOfMemory")); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), code, FE_IR_I16); + } + fe_ir_jmp(L->b, join->id); + L->b = join; + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); +} + +static int lower_mem(Lower *L, FeNode *n, Slot *out) +{ + unsigned args[2]; + if (is_mem_call(n, "create")) { + FeNode *arg = n->children; + FeType *value = arg ? arg->sem_type : 0; + unsigned size = fe_ir_const(L->m, L->b, FE_IR_I32, + (long)ir_size(value)); + unsigned p; + Slot v; + args[0] = size; + p = fe_ir_call(L->m, L->b, FE_IR_PTR, RT_ALLOC, args, 1); + /* The value is written through the new pointer, not copied into a + local first: `create` moves what it was given. */ + v = lower_expr(L, arg); + store_into(L, fe_ir_at_temp(p, 0), v, arg, ir_size(value)); + *out = allocation_result(L, n, p); + return 1; + } + if (is_mem_call(n, "alloc_slice")) { + FeNode *type_arg = n->children; + FeNode *count_arg = type_arg ? type_arg->next : 0; + FeType *t = n->sem_type; + /* `!^[]T` -- the payload is an owned slice, a pointer and a length. */ + FeType *owned = t ? t->error_value : 0; + FeType *slice = owned ? owned->elem : 0; + FeType *elem = slice ? slice->elem : 0; + unsigned each = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + unsigned howmany = count_arg + ? as_value(L, lower_expr(L, count_arg), count_arg) + : fe_ir_const(L->m, L->b, FE_IR_I32, 0); + unsigned bytes = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, + howmany, each, 1); + unsigned p; + unsigned local = scratch(L, t, "allocated"); + long payload_at = (long)fe_type_payload_offset(t); + unsigned zero; + unsigned ok; + FeIrBlock *good; + FeIrBlock *bad; + FeIrBlock *join; + args[0] = bytes; + p = fe_ir_call(L->m, L->b, FE_IR_PTR, RT_ALLOC, args, 1); + zero = fe_ir_const(L->m, L->b, FE_IR_PTR, 0); + ok = fe_ir_binary(L->m, L->b, FE_IR_NE, FE_IR_PTR, p, zero, 1); + good = new_block(L); + bad = new_block(L); + join = new_block(L); + fe_ir_br(L->b, ok, good->id, bad->id); + L->b = good; + { + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + fe_ir_store(L->m, L->b, + fe_ir_at_local(local, payload_at + SLICE_PTR_OFFSET), + p, FE_IR_PTR); + fe_ir_store(L->m, L->b, + fe_ir_at_local(local, payload_at + SLICE_LEN_OFFSET), + howmany, FE_IR_I32); + } + fe_ir_jmp(L->b, join->id); + L->b = bad; + { + unsigned code = fe_ir_const(L->m, L->b, FE_IR_I16, + error_code(L, "OutOfMemory")); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), code, FE_IR_I16); + } + fe_ir_jmp(L->b, join->id); + L->b = join; + *out = slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + return 1; + } + if (is_mem_call(n, "destroy")) { + FeNode *arg = n->children; + Slot p = lower_expr(L, arg); + /* An owned slice is a pointer and a length; what was allocated is the + pointer. */ + if (p.type == FE_IR_MEM) { + FeIrPlace at = p.place; + at.offset += SLICE_PTR_OFFSET; + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); + } else { + args[0] = as_value(L, p, arg); + } + fe_ir_call(L->m, L->b, FE_IR_VOID, RT_FREE, args, 1); + *out = slot_void(); + return 1; + } + if (is_mem_call(n, "replace")) { + /* Read what is there, put the new value in its place, hand back the + old one. This is how a value is taken out of a field without ever + leaving the field uninitialised (SPEC 5 R7). */ + FeNode *dst = n->children; + FeNode *value = dst ? dst->next : 0; + FeType *t = n->sem_type; + unsigned target = as_value(L, lower_expr(L, dst), dst); + unsigned old = scratch(L, t, "replaced"); + Slot fresh; + fe_ir_copy(L->m, L->b, fe_ir_at_local(old, 0), fe_ir_at_temp(target, 0), + ir_size(t)); + fresh = lower_expr(L, value); + store_into(L, fe_ir_at_temp(target, 0), fresh, value, ir_size(t)); + *out = slot_place(fe_ir_at_local(old, 0), ir_type(t), ir_size(t)); + return 1; + } + return 0; +} + static Slot lower_call(Lower *L, FeNode *n) { unsigned args[16]; @@ -462,6 +630,7 @@ static Slot lower_call(Lower *L, FeNode *n) { Slot built; if (lower_builtin(L, n, &built)) return built; + if (lower_mem(L, n, &built)) return built; } if (!callee) { fail(L, "a call with no target", n); return slot_void(); } /* An aggregate result is written through a hidden first argument. */ @@ -650,9 +819,15 @@ static Slot lower_expr_core(Lower *L, FeNode *n) FeType *bt = n->a ? n->a->sem_type : 0; return wrapper_payload(L, lower_expr(L, n->a), bt); } - /* `p.^` reads through a pointer. */ + /* `p.^` reads through a pointer -- except for an owned slice, whose + pointer and length are the value itself, so there is nothing to + step through. */ if (n->text && !strcmp(n->text, ".^")) { - unsigned p = as_value(L, lower_expr(L, n->a), n->a); + Slot base = lower_expr(L, n->a); + unsigned p; + if (base.type == FE_IR_MEM) + return slot_place(base.place, it, ir_size(t)); + p = as_value(L, base, n->a); return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); } /* `.n` is how many elements there are, which an array knows at diff --git a/fec/tests/exec/heap.fe b/fec/tests/exec/heap.fe new file mode 100644 index 0000000..03367b8 --- /dev/null +++ b/fec/tests/exec/heap.fe @@ -0,0 +1,30 @@ +// EXIT:0 +// OUTPUT:sum 4950 +unit heap; +import std.io; +import std.fmt; + +fn build(n: usize) -> !^[]mut i32 { + var cells: ^[]mut i32 = try mem.alloc_slice(i32, n); + var i: usize = 0; + while i < n { + cells.^[i] = i as i32; + i = i + 1; + } + return cells; +} + +fn main() -> i32 { + let cells: ^[]mut i32 = build(100) catch |e| { return 1; }; + defer { mem.destroy(cells); } + var sum: i32 = 0; + for v in cells.^ { + sum = sum + v.^; + } + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let k: usize = fmt.fmt_i32(buf[..], sum); + io.print("sum "); + io.print(buf[0..k]); + io.print("\n"); + return 0; +} From 3a01cb4c51ca2cde2be897c98062be51ba59ea05 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:32:15 +0900 Subject: [PATCH 146/184] =?UTF-8?q?lower:=20=EC=86=8C=EC=9C=A0=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=8A=A4=EC=BD=94=ED=94=84=20=EB=81=9D=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit defer 목록을 '스코프가 아직 갚아야 할 것' 목록으로 일반화했다. defer 블록과 소유 값 해제가 같은 목록에 쓰인 순서대로 들어가고, 모든 이탈 경로가 역순으로 갚는다. 해제에는 값 옆에 플래그를 둔다. 값이 저장될 때 세우고 넘겨줄 때 지운다. 값이 아직 여기 있는 경로에서만 해제되는데, 그건 코드의 모양만 봐서는 알 수 없는 것이다. 검사기가 소유권을 넘기는 사용을 이미 표시해두므로 그것을 읽는다. !void 함수의 빈 return 은 성공이다. 줄 값도 없고 오류도 없다는 뜻인데 프론트엔드가 타입 불일치로 거부하고 있었다. 런타임이 할당/해제 횟수를 센다. owndrop 프로그램이 그 둘이 일치함을 실행으로 증명한다 -- 이른 반환, 이미 넘긴 값, 스코프 끝 전부. --- fec/rt/start.asm | 18 +++++ fec/src/check.c | 5 ++ fec/src/lower.c | 140 ++++++++++++++++++++++++++++++++++---- fec/std/sys.fe | 7 ++ fec/tests/exec/owndrop.fe | 28 ++++++++ 5 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 fec/tests/exec/owndrop.fe diff --git a/fec/rt/start.asm b/fec/rt/start.asm index 340020f..45ead57 100644 --- a/fec/rt/start.asm +++ b/fec/rt/start.asm @@ -28,6 +28,8 @@ colon db ':',0 newline db 13,10,0 numbuf db 16 dup(0) written dd 0 +allocs dd 0 +frees dd 0 _DATA ends @@ -168,6 +170,7 @@ fe_rt_alloc proc near push 8 ; HEAP_ZERO_MEMORY push eax call _HeapAlloc@12 + inc dword ptr [allocs] mov esp, ebp pop ebp ret @@ -186,12 +189,27 @@ fe_rt_free proc near push 0 push eax call _HeapFree@12 + inc dword ptr [frees] free_done: mov esp, ebp pop ebp ret fe_rt_free endp +; fe_rt_allocs() / fe_rt_frees() -- what the allocator has been asked to do, +; so that a test can insist every allocation was released. +public fe_rt_allocs +fe_rt_allocs proc near + mov eax, [allocs] + ret +fe_rt_allocs endp + +public fe_rt_frees +fe_rt_frees proc near + mov eax, [frees] + ret +fe_rt_frees endp + ; fe_rt_exit(code) -- never returns public fe_rt_exit fe_rt_exit proc near diff --git a/fec/src/check.c b/fec/src/check.c index a86cd34..4c6ae41 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -3615,6 +3615,11 @@ static void check_stmt(FeCheckerState *s, FeNode *n) actual && actual->kind==FE_TYPE_ERROR_UNION && !fe_type_equal(expected,actual)) err(s->c,n->loc,"error result type mismatch"); + /* A bare `return` in a function returning `!void` is the success case: + there is no value to give, and no error either. */ + else if (!n->a && expected && expected->kind==FE_TYPE_ERROR_UNION && + expected->error_value && + expected->error_value->kind==FE_TYPE_VOID) { } else if (!fe_type_equal(expected,stored) && !m7_actual_compatible(expected,stored,n->a)) err(s->c,n->loc,"return type mismatch"); diff --git a/fec/src/lower.c b/fec/src/lower.c index ae90687..e6ad699 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -1,6 +1,7 @@ #include "lower.h" #include #include "m7.h" +#include "own.h" #include /* ------------------------------------------------------------------------- * @@ -36,10 +37,21 @@ typedef struct Lower { unsigned break_target[32]; unsigned continue_target[32]; unsigned loop_depth; - /* `defer` blocks in the order they were written. Every exit path runs the - ones that are live, last written first. */ - FeNode *deferred[32]; - unsigned defer_count; + /* What a scope still owes when it ends: `defer` blocks to run and owned + values to release, in the order they were written. Every exit path runs + what is live, last first. + + A drop carries a flag beside the value. The flag is set when the value + is stored and cleared wherever it is moved away, so the release happens + exactly on the paths where the value is still there -- which is not + something the shape of the code can tell you on its own. */ + struct { + FeNode *block; /* a `defer`, when set */ + unsigned local; /* the owned value, otherwise */ + unsigned flag; + FeType *type; + } owed[64]; + unsigned owed_count; /* Every `error.Name` used anywhere in the build, sorted, numbered from one. SPEC 4.6: the names are collected rather than declared, and the order is fixed by the spelling so that the same program always gets the same @@ -194,6 +206,14 @@ static unsigned as_address(Lower *L, Slot s, FeNode *n) /* --------------------------------------------------------------- locals --- */ +/* Does letting go of this type have to do something? */ +static int needs_release(const FeType *t) +{ + if (!t) return 0; + if (t->kind == FE_TYPE_OWNED) return 1; + return t->has_drop != 0; +} + static unsigned declare_var(Lower *L, const char *cname, const FeType *t, const char *name) { @@ -205,9 +225,31 @@ static unsigned declare_var(Lower *L, const char *cname, const FeType *t, L->vars[L->var_count].by_address = 0; ++L->var_count; } + if (needs_release(t) && L->owed_count < 64) { + unsigned flag = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "live"); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); + L->owed[L->owed_count].block = 0; + L->owed[L->owed_count].local = local; + L->owed[L->owed_count].flag = flag; + L->owed[L->owed_count].type = (FeType *)t; + ++L->owed_count; + } return local; } +/* The liveness flag beside a local, or none. */ +static int release_flag(Lower *L, unsigned local, unsigned *flag) +{ + unsigned i; + for (i = L->owed_count; i > 0; --i) + if (!L->owed[i - 1].block && L->owed[i - 1].local == local) { + *flag = L->owed[i - 1].flag; + return 1; + } + return 0; +} + static LowerVar *find_var(Lower *L, const char *cname) { unsigned i; @@ -687,6 +729,16 @@ static Slot lower_expr(Lower *L, FeNode *n) Slot v; if (!n || L->failed) return slot_void(); v = lower_expr_core(L, n); + /* The checker marked the uses that hand ownership away. Where one names a + local we track, the value is no longer ours to release. */ + if ((n->flags & FE_OWN_NODE_CONSUMED) && n->kind == FE_N_IDENT) { + LowerVar *var = find_var(L, n->cname); + unsigned flag; + if (var && release_flag(L, var->local, &flag)) { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); + } + } return n->sem_context ? wrap_context(L, v, n) : v; } @@ -938,12 +990,40 @@ static Slot lower_expr_core(Lower *L, FeNode *n) } } -/* Run the `defer` blocks that are live, most recent first. A `return` in the - middle of a function still owes them, so every exit path calls this. */ +/* Settle what a scope owes, most recent first. A `return` in the middle of a + function still owes everything, so every exit path calls this. */ static void run_deferred(Lower *L, unsigned from) { unsigned i; - for (i = L->defer_count; i > from; --i) lower_stmt(L, L->deferred[i - 1]); + for (i = L->owed_count; i > from; --i) { + if (L->owed[i - 1].block) { + lower_stmt(L, L->owed[i - 1].block); + continue; + } + { + /* Release only where the value is still here. */ + unsigned live = fe_ir_load(L->m, L->b, FE_IR_I8, + fe_ir_at_local(L->owed[i - 1].flag, 0)); + FeIrBlock *doit = new_block(L); + FeIrBlock *skip = new_block(L); + unsigned args[1]; + FeType *t = L->owed[i - 1].type; + fe_ir_br(L->b, live, doit->id, skip->id); + L->b = doit; + if (t && t->kind == FE_TYPE_OWNED && t->elem && + t->elem->kind == FE_TYPE_SLICE) { + FeIrPlace at = fe_ir_at_local(L->owed[i - 1].local, + SLICE_PTR_OFFSET); + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); + } else { + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->owed[i - 1].local, 0)); + } + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); + fe_ir_jmp(L->b, skip->id); + L->b = skip; + } + } } /* ------------------------------------------------------- wrappers -------- * @@ -1116,7 +1196,30 @@ static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, static void lower_return(Lower *L, FeNode *n) { Slot v; - if (!n->a) { run_deferred(L, 0); fe_ir_ret(L->b, 0, 0); return; } + if (!n->a) { + /* A bare return from a `!void` function still has to say that nothing + went wrong. */ + if (L->ret_type && L->ret_type->kind == FE_TYPE_ERROR_UNION) { + unsigned local = scratch(L, L->ret_type, "success"); + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), + fe_ir_at_local(local, 0), ir_size(L->ret_type)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(L->ret_type), + fe_ir_at_local(local, 0)), 1); + return; + } + run_deferred(L, 0); + fe_ir_ret(L->b, 0, 0); + return; + } /* The value is computed before the deferred blocks run, because they may destroy what it was read from. */ v = lower_expr(L, n->a); @@ -1400,12 +1503,12 @@ static void lower_stmt(Lower *L, FeNode *n) if (!n || L->failed) return; switch (n->kind) { case FE_N_BLOCK: { - unsigned outer = L->defer_count; + unsigned outer = L->owed_count; for (x = n->children; x; x = x->next) lower_stmt(L, x); - /* Leaving a block normally runs what it deferred. An exit that jumped - away already ran them on its way out. */ + /* Leaving a block normally settles what it owes. An exit that jumped + away already settled on its way out. */ if (!L->b->terminated) run_deferred(L, outer); - L->defer_count = outer; + L->owed_count = outer; return; } case FE_N_LET: @@ -1414,7 +1517,12 @@ static void lower_stmt(Lower *L, FeNode *n) unsigned local = declare_var(L, n->cname, n->sem_type, n->text); if (n->b) { Slot v = lower_expr(L, n->b); + unsigned flag; store_into(L, fe_ir_at_local(local, 0), v, n, ir_size(n->sem_type)); + if (release_flag(L, local, &flag)) { + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), one, FE_IR_I8); + } } return; } @@ -1448,7 +1556,13 @@ static void lower_stmt(Lower *L, FeNode *n) lower_stmt(L, n->a); return; case FE_N_DEFER: - if (L->defer_count < 32) L->deferred[L->defer_count++] = n->a; + if (L->owed_count < 64) { + L->owed[L->owed_count].block = n->a; + L->owed[L->owed_count].local = 0; + L->owed[L->owed_count].flag = 0; + L->owed[L->owed_count].type = 0; + ++L->owed_count; + } return; case FE_N_FOR: lower_for(L, n); diff --git a/fec/std/sys.fe b/fec/std/sys.fe index 18088b8..c2c91f9 100644 --- a/fec/std/sys.fe +++ b/fec/std/sys.fe @@ -6,6 +6,8 @@ extern "c" fn fe_rt_write(handle: i32, bytes: *u8, len: usize) -> i32; extern "c" fn fe_rt_alloc(n: usize) -> *u8; extern "c" fn fe_rt_free(p: *u8); extern "c" fn fe_rt_exit(code: i32); +extern "c" fn fe_rt_allocs() -> i32; +extern "c" fn fe_rt_frees() -> i32; pub fn exit(code: i32) -> void { unsafe { fe_rt_exit(code); } @@ -22,3 +24,8 @@ pub fn raw_alloc(n: usize) -> *u8 { pub fn raw_free(p: *u8) -> void { unsafe { fe_rt_free(p); } } + +// How many times the allocator was asked to hand out memory, and to take it +// back. A test can insist the two agree; nothing else should care. +pub fn allocs() -> i32 { unsafe { return fe_rt_allocs(); } } +pub fn frees() -> i32 { unsafe { return fe_rt_frees(); } } diff --git a/fec/tests/exec/owndrop.fe b/fec/tests/exec/owndrop.fe new file mode 100644 index 0000000..f12cabd --- /dev/null +++ b/fec/tests/exec/owndrop.fe @@ -0,0 +1,28 @@ +// EXIT:0 +// OUTPUT:balanced +unit owndrop; +import std.io; +import std.sys; + +fn take(p: ^i32) -> void { mem.destroy(p); } + +fn scoped() -> !void { + let a: ^i32 = try mem.create(1); + let b: ^i32 = try mem.create(2); + take(b); +} + +fn early(flag: bool) -> !void { + let p: ^i32 = try mem.create(3); + if flag { return; } + take(p); +} + +fn main() -> i32 { + scoped() catch |e| { return 1; }; + early(true) catch |e| { return 2; }; + early(false) catch |e| { return 3; }; + if sys.allocs() == sys.frees() { io.print("balanced\n"); return 0; } + io.print("leaked\n"); + return 4; +} From b0c9338cf3bbb4a03529a82d6c381c945e1c59eb Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:32:57 +0900 Subject: [PATCH 147/184] =?UTF-8?q?tests:=20pending-backend=20=EA=B2=A9?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20=EC=97=86=EC=95=A4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 코드 생성기가 없어서 돌릴 수 없던 것들이다. 이제 돌아간다. 경계 트랩, --no-checks 차등, 슬라이스 범위 검사, 소유권 해제가 전부 exec/ 에서 실행으로 검증되므로 격리할 이유가 없다. run.py 205/205, exec.py 17/17. --- fec/tests/exec/sliceok.fe | 14 +++ fec/tests/exec/slicerng.fe | 11 ++ fec/tests/pending-backend/README.md | 10 -- fec/tests/pending-backend/bounds_nocheck.fe | 7 -- fec/tests/pending-backend/bounds_trap.fe | 6 -- fec/tests/pending-backend/format-prop.c | 11 -- fec/tests/pending-backend/ownership-drop.c | 101 ------------------ fec/tests/pending-backend/ownership_drop.fe | 100 ----------------- .../pending-backend/slice_bounds_trap.fe | 7 -- tests/run.py | 15 ++- 10 files changed, 31 insertions(+), 251 deletions(-) create mode 100644 fec/tests/exec/sliceok.fe create mode 100644 fec/tests/exec/slicerng.fe delete mode 100644 fec/tests/pending-backend/README.md delete mode 100644 fec/tests/pending-backend/bounds_nocheck.fe delete mode 100644 fec/tests/pending-backend/bounds_trap.fe delete mode 100644 fec/tests/pending-backend/format-prop.c delete mode 100644 fec/tests/pending-backend/ownership-drop.c delete mode 100644 fec/tests/pending-backend/ownership_drop.fe delete mode 100644 fec/tests/pending-backend/slice_bounds_trap.fe diff --git a/fec/tests/exec/sliceok.fe b/fec/tests/exec/sliceok.fe new file mode 100644 index 0000000..43bd34a --- /dev/null +++ b/fec/tests/exec/sliceok.fe @@ -0,0 +1,14 @@ +// EXIT:9 +unit sliceok; + +fn total(s: []i32) -> i32 { + var sum: i32 = 0; + for v in s { sum = sum + v.^; } + return sum; +} + +fn main() -> i32 { + let a: [5]i32 = [1, 2, 3, 4, 5]; + let mid: []i32 = a[1..4]; + return total(mid); +} diff --git a/fec/tests/exec/slicerng.fe b/fec/tests/exec/slicerng.fe new file mode 100644 index 0000000..78aef2f --- /dev/null +++ b/fec/tests/exec/slicerng.fe @@ -0,0 +1,11 @@ +// EXIT:3 +// OUTPUT:index out of bounds +// NOCHECKS:0 +unit slicerng; + +fn main() -> i32 { + let a: [2]i32 = [1, 2]; + let s: []i32 = a[0..3]; + let n: i32 = s.n as i32; + return n - n; +} diff --git a/fec/tests/pending-backend/README.md b/fec/tests/pending-backend/README.md deleted file mode 100644 index a653444..0000000 --- a/fec/tests/pending-backend/README.md +++ /dev/null @@ -1,10 +0,0 @@ -`fec/tests/pending-backend/`에 남겨둔 fixture는 현재 프론트엔드 전용 상태에서 실행할 수 없습니다. - -- `bounds_trap.fe` / `bounds_nocheck.fe`: 경계 검사 실패가 실제로 trap되는지, - `--no-checks` 플래그가 그 검사를 제거하는지 확인하는 런타임 동작 테스트입니다. -- `ownership_drop.fe` + `ownership-drop.c`: 삽입된 `drop`/`defer`가 실제로 실행되는지 확인하는 테스트입니다. - `ownership-drop.c`는 해제 횟수/순서/이중 해제를 검증하는 하네스입니다. -- `format-prop.c`: 포맷 프로퍼티 동작을 확인하는 런타임 검사입니다. - -이들은 코드 생성기가 없는 현재 단계에서는 실행할 수 없어서 `tests/run.py`가 건너뜁니다. -백엔드(코드 생성기)가 돌아오면 가장 먼저 재활성화할 대상입니다. diff --git a/fec/tests/pending-backend/bounds_nocheck.fe b/fec/tests/pending-backend/bounds_nocheck.fe deleted file mode 100644 index e8e62f3..0000000 --- a/fec/tests/pending-backend/bounds_nocheck.fe +++ /dev/null @@ -1,7 +0,0 @@ -unit m3_no_checks; - -fn main() -> i32 { - let a: [2]i32 = [1, 2]; - let x: i32 = a[2]; - return x - x; -} diff --git a/fec/tests/pending-backend/bounds_trap.fe b/fec/tests/pending-backend/bounds_trap.fe deleted file mode 100644 index ac581ee..0000000 --- a/fec/tests/pending-backend/bounds_trap.fe +++ /dev/null @@ -1,6 +0,0 @@ -unit m3_bounds; - -fn main() -> i32 { - let a: [2]i32 = [1, 2]; - return a[2]; -} diff --git a/fec/tests/pending-backend/format-prop.c b/fec/tests/pending-backend/format-prop.c deleted file mode 100644 index 66bba1a..0000000 --- a/fec/tests/pending-backend/format-prop.c +++ /dev/null @@ -1,11 +0,0 @@ -#include "prop.c" - -int main(void) -{ - fe_writer w; - unsigned short result; - w.tag=2; - w.handle=99; - result=fe_m4_prop_propagate(w); - return result==1 ? 0 : 1; -} diff --git a/fec/tests/pending-backend/ownership-drop.c b/fec/tests/pending-backend/ownership-drop.c deleted file mode 100644 index 5d3e6bb..0000000 --- a/fec/tests/pending-backend/ownership-drop.c +++ /dev/null @@ -1,101 +0,0 @@ -#include -#include - -#undef malloc -#undef free - -extern void *malloc(size_t size); -extern void free(void *p); - -/* `run` returns !i32, which lowers to { error, value }. */ -struct fe_result_value_9 { unsigned short e; long v; }; -extern struct fe_result_value_9 fe_m5_runtime_run(long mode); -extern unsigned short fe_m5_runtime_conditional(unsigned char flag); -extern unsigned short fe_m5_runtime_argument_cleanup(void); -extern unsigned short fe_m5_runtime_owned_slice(unsigned long n); -extern unsigned short fe_m5_runtime_replace_field(void); -extern unsigned short fe_m5_runtime_loop_cleanup(void); -extern unsigned short fe_m5_runtime_try_cleanup(void); -extern unsigned short fe_m5_runtime_field_order(void); -extern unsigned short fe_m5_runtime_defer_order(void); -extern unsigned short fe_m5_runtime_match_cleanup(unsigned char flag); -extern unsigned short fe_m5_runtime_close_once(void); -extern unsigned short fe_m5_runtime_reassign_struct(void); - -static void *live_ptrs[64]; -static unsigned live_count; -static unsigned alloc_count; -static unsigned free_count; -static unsigned double_free_count; -static long fail_after = -1; -static unsigned malloc_attempts; -static int track_order; -static void *order_ptrs[2]; -static unsigned order_allocs; -static unsigned order_frees; -static unsigned order_bad; - -void *m5_malloc(size_t size) -{ - void *p; - if (fail_after >= 0 && (long)malloc_attempts++ == fail_after) return 0; - p = malloc(size); - if (p && live_count < 64) live_ptrs[live_count++] = p; - if (p) ++alloc_count; - if (p && track_order && order_allocs < 2) order_ptrs[order_allocs++] = p; - return p; -} - -void m5_free(void *p) -{ - unsigned i; - if (!p) return; - for (i = 0; i < live_count; ++i) { - if (live_ptrs[i] == p) { - if (track_order && order_frees < 2 && - p != order_ptrs[1-order_frees]) ++order_bad; - if (track_order && order_frees < 2) ++order_frees; - live_ptrs[i] = live_ptrs[--live_count]; - ++free_count; - free(p); - return; - } - } - ++double_free_count; -} - -int main(void) -{ - struct fe_result_value_9 r; - r = fe_m5_runtime_run(0); if (r.e != 0 || r.v != 0) return 1; - r = fe_m5_runtime_run(1); if (r.e != 0 || r.v != 9) return 2; - r = fe_m5_runtime_run(2); if (r.e != 0 || r.v != 0) return 3; - if (fe_m5_runtime_conditional(0) != 0) return 4; - if (fe_m5_runtime_conditional(1) != 0) return 5; - if (fe_m5_runtime_argument_cleanup() != 0) return 6; - if (fe_m5_runtime_owned_slice(17) != 0) return 7; - if (fe_m5_runtime_replace_field() != 0) return 8; - if (fe_m5_runtime_loop_cleanup() != 0) return 9; - fail_after=1; - malloc_attempts=0; - if (fe_m5_runtime_try_cleanup() == 0) return 10; - fail_after=-1; - track_order=1; - order_allocs=order_frees=order_bad=0; - if (fe_m5_runtime_field_order() != 0) return 11; - track_order=0; - if (order_allocs != 2 || order_frees != 2 || order_bad != 0) return 12; - track_order=1; - order_allocs=order_frees=order_bad=0; - if (fe_m5_runtime_defer_order() != 0) return 13; - track_order=0; - if (order_allocs != 2 || order_frees != 2 || order_bad != 0) return 14; - if (fe_m5_runtime_match_cleanup(0) != 0) return 15; - if (fe_m5_runtime_match_cleanup(1) != 0) return 16; - if (fe_m5_runtime_close_once() != 0) return 17; - if (fe_m5_runtime_reassign_struct() != 0) return 18; - if (double_free_count != 0) return 19; - if (live_count != 0) return 20; - if (alloc_count != free_count) return 21; - return 0; -} diff --git a/fec/tests/pending-backend/ownership_drop.fe b/fec/tests/pending-backend/ownership_drop.fe deleted file mode 100644 index b9dab0c..0000000 --- a/fec/tests/pending-backend/ownership_drop.fe +++ /dev/null @@ -1,100 +0,0 @@ -unit m5_runtime; - -fn take(p: ^i32) -> void { mem.destroy(p); } - -pub fn run(mode: i32) -> !i32 { - var p: ^i32 = try mem.create(0); - defer { mem.destroy(p); } - p.^ = 7; - if mode == 1 { - p = try mem.create(0); - p.^ = 9; - return p.^; - } - while true { break; } - if mode == 2 { return 0; } - return p.^ - 7; -} - -pub fn conditional(flag: bool) -> !void { - var p: ^i32 = try mem.create(0); - if flag { take(p); } -} - -pub fn argument_cleanup() -> !void { - let p: ^i32 = try mem.create(0); - take(p); -} - -pub fn owned_slice(n: usize) -> !void { - let bytes: ^[]u8 = try mem.alloc_slice(u8, n); -} - -struct Holder { p: ^i32 } - -pub fn replace_field() -> !void { - let first: ^i32 = try mem.create(1); - var h: Holder = Holder{ p: first }; - let second: ^i32 = try mem.create(2); - let old: ^i32 = mem.replace(&mut h.p, second); - mem.destroy(old); -} - -pub fn loop_cleanup() -> !void { - var i: i32 = 0; - while i < 2 { - let p: ^i32 = try mem.create(i); - i += 1; - if i == 1 { continue; } - break; - } -} - -pub fn try_cleanup() -> !void { - let first: ^i32 = try mem.create(1); - let second: ^i32 = try mem.create(2); -} - -struct PairOwners { first: ^i32, second: ^i32 } - -pub fn field_order() -> !void { - let first: ^i32 = try mem.create(1); - let second: ^i32 = try mem.create(2); - let pair: PairOwners = PairOwners{ first: first, second: second }; -} - -pub fn defer_order() -> !void { - let first: ^i32 = try mem.create(1); - defer { mem.destroy(first); } - let second: ^i32 = try mem.create(2); -} - -enum Choice { A, B } - -pub fn match_cleanup(flag: bool) -> !void { - var choice: Choice = Choice.A; - if flag { choice = Choice.B; } - let p: ^i32 = try mem.create(1); - match choice { - A => { take(p); } - B => { take(p); } - } -} - -struct FileLike { - handle: i32, - fn close(self: Self) -> !void { self.handle = 0; } - fn drop(self: &mut Self) { self.handle = 0; } -} - -pub fn close_once() -> !void { - let file: FileLike = FileLike{ handle: 7 }; - try file.close(); -} - -pub fn reassign_struct() -> !void { - let first: ^i32 = try mem.create(1); - var owner: Holder = Holder{ p: first }; - let second: ^i32 = try mem.create(2); - owner = Holder{ p: second }; -} diff --git a/fec/tests/pending-backend/slice_bounds_trap.fe b/fec/tests/pending-backend/slice_bounds_trap.fe deleted file mode 100644 index 89a8d88..0000000 --- a/fec/tests/pending-backend/slice_bounds_trap.fe +++ /dev/null @@ -1,7 +0,0 @@ -unit m3_slice_bounds; - -fn main() -> i32 { - let a: [2]i32 = [1, 2]; - let s: []i32 = a[0..3]; - return s.n as i32; -} diff --git a/tests/run.py b/tests/run.py index f4d3575..4e50fdd 100644 --- a/tests/run.py +++ b/tests/run.py @@ -1,9 +1,8 @@ """Run every fixture through the front end and check what it reports. -The compiler is a front end now -- lexer, parser, types, ownership -- so a -fixture is checked by running `fec` on it and looking at two things: whether it -was accepted, and, when it was rejected, whether the diagnostic is the one the -fixture asked for. +A fixture is checked by running `fec` on it and looking at two things: whether +it was accepted, and, when it was rejected, whether the diagnostic is the one +the fixture asked for. A fixture states its expectation in its first line: @@ -17,8 +16,8 @@ not pin the message yet. Anything else must be accepted. Fixtures under `parse/` are checked with --dump-ast rather than --check: they exercise the grammar, and several are deliberately not well-typed. -This runs on the host in about a second. There is no VM: nothing here executes -generated code, because there is no code generator. +This runs on the host in about a second. It checks what the compiler says; what +the compiled programs actually do is `exec.py`. """ from __future__ import annotations @@ -35,8 +34,6 @@ FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", "check", "resolve", "ir", "lower", "x86", "driver") -# Fixtures live here until there is a code generator to run them against. -QUARANTINE = "pending-backend" MARKER = re.compile(r"^//\s*ERROR:(?:(\d+):)?(.*)$") @@ -116,7 +113,7 @@ def main() -> int: args = ap.parse_args() fec = build(ROOT / ".build") - cases = sorted(p for p in FIXTURES.rglob("*.fe") if QUARANTINE not in p.parts) + cases = sorted(FIXTURES.rglob("*.fe")) if args.select: cases = [p for p in cases if args.select in p.as_posix()] From 206799d1cb56b24f7ff035b9b0f7943f9db5fbbf Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:42:16 +0900 Subject: [PATCH 148/184] =?UTF-8?q?std:=20str=20=EA=B3=BC=20list,=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EC=9E=90=EB=8F=99=20drop=20?= =?UTF-8?q?=EC=9D=B4=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EA=B9=8C=EC=A7=80=20=EB=8B=BF=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std.str 은 eq/starts_with/find/trim/parse_int 을 바이트 슬라이스 위에서 한다. std.list 는 자라는 제네릭 시퀀스다 -- 버퍼를 소유하므로 리스트를 놓으면 원소도 놓인다. 성장은 두 배씩이라 push 당 복사량이 상수로 눌린다. 찾은 버그 넷: - 메서드가 자기 타입의 유닛이 아니라 호출한 유닛에 속한 것으로 계산됐다. 다른 유닛의 제네릭 타입을 쓰면 필드가 전부 private 으로 보였다. - 참조로 도달한 메서드를 찾지 못했다. self.grow() 가 안 됐다. - 이미 참조인 수신자의 주소를 한 번 더 떠서 넘겼다. 포인터의 포인터를 받은 메서드가 그것을 구조체로 읽었다. - 유닛으로 한정된 제네릭 타입(list.List(i32))이 타입 자리에서도 식 자리에서도 해석되지 않았다. drop 을 가진 타입은 인스턴스마다 그 메서드가 존재해야 한다 -- 이름으로 부르는 사람이 없어도 스코프 정리가 부른다. 그리고 자기 drop 안에서는 필드를 꺼낼 수 있다. 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다. run.py 207/207, exec.py 19/19. --- fec/src/check.c | 101 ++++++++++++++++++++++++++++++++++---- fec/src/lower.c | 41 ++++++++++++++-- fec/std/list.fe | 53 ++++++++++++++++++-- fec/std/str.fe | 69 ++++++++++++++++++++++++-- fec/tests/exec/listuse.fe | 42 ++++++++++++++++ fec/tests/exec/strings.fe | 29 +++++++++++ 6 files changed, 317 insertions(+), 18 deletions(-) create mode 100644 fec/tests/exec/listuse.fe create mode 100644 fec/tests/exec/strings.fe diff --git a/fec/src/check.c b/fec/src/check.c index 4c6ae41..5da3711 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -70,9 +70,27 @@ static int known(FeType *t) return t && t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ERROR; } +/* Is this a projection of `self` inside that type's own `drop`? */ +static int in_own_drop(FeCheckerState *s, FeNode *n) +{ + FeNode *base; + if (!s->fn_node || !s->fn_node->text || strcmp(s->fn_node->text,"drop")!=0) + return 0; + base = n ? n->a : 0; + while (base && (base->kind==FE_N_MEMBER || base->kind==FE_N_INDEX)) + base = base->a; + return base && base->kind==FE_N_IDENT && base->text && + strcmp(base->text,"self")==0; +} + static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) { FeSym *sym=0; + /* Inside a type's own `drop` the object is going away, so taking a field + out of it leaves nothing behind that anyone could read. That is the one + place R7 has nothing to protect. */ + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX) && in_own_drop(s,n)) + return; if (n && n->kind==FE_N_IDENT) sym=find_symbol(s->scope,n->text ? n->text : ""); if (s->defer_depth != 0) { @@ -1283,6 +1301,12 @@ static FeType *check_expr_core(FeCheckerState *s, FeNode *n) } } et=check_expr(s,n->a->a); + /* A method can be reached through a reference or an owner as well + as through the value itself. */ + if (et && (et->kind==FE_TYPE_REF || et->kind==FE_TYPE_OWNED) && + et->elem && et->elem->kind==FE_TYPE_STRUCT && + find_method(c,et->elem,n->a->b ? n->a->b->text : "")) + et=et->elem; method=et && et->kind==FE_TYPE_STRUCT ? find_method(c,et,n->a->b ? n->a->b->text : "") : 0; if(method) { @@ -2361,6 +2385,23 @@ static FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, pop_bindings(c,&save); } fe_type_layout_all(&c->types); + /* A type that says how to let go of itself needs that method to exist for + every instance, whether or not anyone calls it by name: scope cleanup + will. */ + { + FeNode *release; + for (release=decl->children;release;release=release->next) + if (release->kind==FE_N_FN && release->text && + !strcmp(release->text,"drop") && release->c) { + FeCheckerState s; + memset(&s,0,sizeof s); + s.c=c; + s.scope=c->unit_scope[unit_index(c,home)]; + s.globals=s.scope; + check_instance_method(&s,t,release,decl->loc,0); + break; + } + } return t; } @@ -2391,18 +2432,30 @@ static FeType *instantiate_type_node(void *owner, const FeNode *node) { FeCheck *c=(FeCheck *)owner; FeUnit *home=current_unit(c); + const char *name=node->text; FeNode *arg; FeType *args[FE_TYPE_PARAM_MAX]; unsigned count=0; FeType *result; + /* `binding.Name` names a type in another unit. The binding is not itself a + type, so it has to be peeled off before anything is looked up. */ + if (node->a && node->a->kind==FE_N_IDENT && node->a->text && c->build && + c->unit) { + FeUnit *bound=fe_build_binding(c->build,c->unit,node->text); + if (bound) { home=bound; name=node->a->text; } + } if (!node->children) { - /* A generic declaration is not a type until it has its arguments. */ - FeNode *decl=unit_type_decl(c,home,node->text ? node->text : ""); + FeNode *decl=unit_type_decl(c,home,name ? name : ""); if (decl && decl_is_generic(decl)) { + /* A generic declaration is not a type until it has arguments. */ err(c,node->loc,"generic type requires type arguments"); return unknown(c); } - return fe_type_intern(&c->types,node->text); + if (name!=node->text) { + FeType *there=unit_type(c,home,name); + if (there) return there; + } + return fe_type_intern(&c->types,name); } if (!instance_descend(c,node->loc)) return unknown(c); for (arg=node->children;arg;arg=arg->next) { @@ -2415,7 +2468,7 @@ static FeType *instantiate_type_node(void *owner, const FeNode *node) --c->instance_depth; return unknown(c); } - result=instantiate_struct(c,home,node->text ? node->text : "",args,count, + result=instantiate_struct(c,home,name ? name : "",args,count, node->loc); --c->instance_depth; return result; @@ -2448,13 +2501,27 @@ static FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) if (t && t->kind!=FE_TYPE_UNKNOWN) { *ok=1; return t; } return unknown(c); } - if (n->kind==FE_N_CALL && n->a && n->a->kind==FE_N_IDENT && n->a->text) { + if (n->kind==FE_N_CALL && n->a && + (n->a->kind==FE_N_IDENT || + (n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->b && n->a->b->text))) { FeType *args[FE_TYPE_PARAM_MAX]; unsigned count=0; FeNode *arg; FeType *result; FeUnit *home=current_unit(c); - if (!unit_type_decl(c,home,n->a->text)) return unknown(c); + const char *want; + /* `Name(args)` here, `binding.Name(args)` when the declaration is in + another unit. */ + if (n->a->kind==FE_N_MEMBER) { + FeUnit *bound=binding_unit(s,n->a->a); + if (!bound) return unknown(c); + home=bound; + want=n->a->b->text; + } else { + want=n->a->text; + } + if (!want || !unit_type_decl(c,home,want)) return unknown(c); if (!instance_descend(c,n->loc)) { *ok=1; return unknown(c); } for (arg=n->children;arg;arg=arg->next) { int inner=0; @@ -2464,7 +2531,7 @@ static FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) ++count; } if (count>FE_TYPE_PARAM_MAX) { --c->instance_depth; return unknown(c); } - result=instantiate_struct(c,home,n->a->text,args,count,n->loc); + result=instantiate_struct(c,home,want,args,count,n->loc); --c->instance_depth; *ok=1; return result; @@ -2578,17 +2645,30 @@ static FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, /* `Type.method(...)` where Type is a generic instance and the method takes no self parameter. */ +/* The unit a name belongs to, by name. */ +static FeUnit *unit_named(FeCheck *c, const char *name) +{ + unsigned u; + if (!name) return 0; + for (u=0;ubuild->count;++u) + if (!strcmp(c->build->units[u].name,name)) return &c->build->units[u]; + return 0; +} + static FeType *check_static_method_call(FeCheckerState *s, FeNode *n, FeType *owner, FeNode *method) { FeCheck *c=s->c; - FeUnit *home=current_unit(c); + /* A method belongs to the unit that declared its type, not to whichever + unit happens to be calling it. */ + FeUnit *home=unit_named(c,owner ? owner->unit : 0); FeBindSave save; FeType *result; char key[FE_GENERIC_KEY_MAX]; FeType *self_args[1]; int fresh; FeSym fake; + if (!home) home=current_unit(c); self_args[0]=owner; instance_key(key,home->name,method->text,self_args,1); memset(&fake,0,sizeof fake); @@ -2618,11 +2698,14 @@ static void check_instance_method(FeCheckerState *s, FeType *owner, FeNode *method, FeLoc site, FeNode *call) { FeCheck *c=s->c; - FeUnit *home=current_unit(c); + /* A method belongs to the unit that declared its type, not to whichever + unit happens to be calling it. */ + FeUnit *home=unit_named(c,owner ? owner->unit : 0); FeBindSave save; char key[FE_GENERIC_KEY_MAX]; FeType *self_args[1]; self_args[0]=owner; + if (!home) home=current_unit(c); instance_key(key,home->name,method->text,self_args,1); { FeBindSave probe; diff --git a/fec/src/lower.c b/fec/src/lower.c index e6ad699..07c2e08 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -687,9 +687,17 @@ static Slot lower_call(Lower *L, FeNode *n) if (n->a && n->a->kind == FE_N_MEMBER && n->sem_decl) { FeNode *first = n->sem_decl->a ? n->sem_decl->a->children : 0; if (first && first->text && !strcmp(first->text, "self")) { + FeType *rt = n->a->a ? n->a->a->sem_type : 0; Slot recv = lower_expr(L, n->a->a); - args[count++] = recv.is_place ? as_address(L, recv, n->a->a) - : recv.temp; + /* A receiver that is already a reference or an owner is a pointer + already; taking its address would pass a pointer to the + pointer. */ + if (rt && (rt->kind == FE_TYPE_REF || + (rt->kind == FE_TYPE_OWNED && ir_type(rt) == FE_IR_PTR))) + args[count++] = as_value(L, recv, n->a->a); + else + args[count++] = recv.is_place ? as_address(L, recv, n->a->a) + : recv.temp; } } /* A generic call passes its type arguments first. They were consumed when @@ -990,6 +998,24 @@ static Slot lower_expr_core(Lower *L, FeNode *n) } } +/* The link name of the `drop` method for this type, found through the instance + the checker recorded. */ +static const char *drop_name(Lower *L, const FeType *t) +{ + unsigned i; + FeNode *method = 0; + if (!t || !t->decl_node) return 0; + for (method = t->decl_node->children; method; method = method->next) + if (method->kind == FE_N_FN && method->text && + !strcmp(method->text, "drop")) break; + if (!method) return 0; + for (i = 0; i < L->c->instance_count; ++i) + if (L->c->instances[i].decl == method && + L->c->instances[i].owner == t) + return L->c->instances[i].cname; + return method->cname; +} + /* Settle what a scope owes, most recent first. A `return` in the middle of a function still owes everything, so every exit path calls this. */ static void run_deferred(Lower *L, unsigned from) @@ -1019,7 +1045,16 @@ static void run_deferred(Lower *L, unsigned from) args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, fe_ir_at_local(L->owed[i - 1].local, 0)); } - fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); + if (t && t->has_drop) { + /* A type that says how to let go of itself is asked to; the + name is the one its instance was given. */ + const char *how = drop_name(L, t); + args[0] = fe_ir_addr(L->m, L->b, + fe_ir_at_local(L->owed[i - 1].local, 0)); + if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1); + } else { + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); + } fe_ir_jmp(L->b, skip->id); L->b = skip; } diff --git a/fec/std/list.fe b/fec/std/list.fe index d414df7..58fff67 100644 --- a/fec/std/list.fe +++ b/fec/std/list.fe @@ -1,6 +1,53 @@ -unit list; +unit std.list; + +// A growable sequence. The buffer is owned, so a List owns its elements and +// releasing it releases them (SPEC 5 R1). Growth doubles, which keeps the +// total copying proportional to the number of pushes. + pub struct List(T) { - items: ^[]T, + items: ^[]mut T, len: usize, - pub fn at(self: &Self, i: usize) -> &T; + + pub fn with_capacity(n: usize) -> !Self { + let room: ^[]mut T = try mem.alloc_slice(T, n); + return Self{ items: room, len: 0 }; + } + + pub fn count(self: &Self) -> usize { return self.len; } + + pub fn at(self: &Self, i: usize) -> T { + return self.items.^[i]; + } + + pub fn set(self: &mut Self, i: usize, v: T) -> void { + self.items.^[i] = v; + } + + pub fn push(self: &mut Self, v: T) -> !void { + if self.len == self.items.^.n { try self.grow(); } + self.items.^[self.len] = v; + self.len = self.len + 1; + return; + } + + /// Move to a buffer twice the size. Kept apart from `push` because the + /// borrow that hands over the old buffer must not be live while the old + /// buffer is still being read (SPEC 5 R6). + fn grow(self: &mut Self) -> !void { + var room: usize = self.items.^.n * 2; + if room == 0 { room = 4; } + let bigger: ^[]mut T = try mem.alloc_slice(T, room); + var i: usize = 0; + while i < self.len { + bigger.^[i] = self.items.^[i]; + i = i + 1; + } + let old: ^[]mut T = mem.replace(&mut self.items, bigger); + mem.destroy(old); + return; + } + + pub fn drop(self: &mut Self) -> void { + mem.destroy(self.items); + } } diff --git a/fec/std/str.fe b/fec/std/str.fe index 4d5d7f0..0e4dbc5 100644 --- a/fec/std/str.fe +++ b/fec/std/str.fe @@ -1,3 +1,66 @@ -unit str; -pub fn eq(a: str, b: str) -> bool; -pub fn trim(s: str) -> str; +unit std.str; + +// `str` is `[]u8` (SPEC 4.2), so these take and give plain byte slices. + +pub fn eq(a: []u8, b: []u8) -> bool { + if a.n != b.n { return false; } + var i: usize = 0; + while i < a.n { + if a[i] != b[i] { return false; } + i = i + 1; + } + return true; +} + +pub fn starts_with(s: []u8, prefix: []u8) -> bool { + if prefix.n > s.n { return false; } + return eq(s[0..prefix.n], prefix); +} + +/// Where `needle` first appears in `s`, or the length of `s` when it does not. +/// An index past the end is how "not found" is said without an optional. +pub fn find(s: []u8, needle: []u8) -> usize { + if needle.n == 0 { return 0; } + if needle.n > s.n { return s.n; } + var at: usize = 0; + let last: usize = s.n - needle.n; + while at <= last { + if eq(s[at..at + needle.n], needle) { return at; } + at = at + 1; + } + return s.n; +} + +pub fn trim(s: []u8) -> []u8 { + var from: usize = 0; + var to: usize = s.n; + while from < to { + if s[from] != 32 and s[from] != 9 and s[from] != 10 and s[from] != 13 { + break; + } + from = from + 1; + } + while to > from { + let c: u8 = s[to - 1]; + if c != 32 and c != 9 and c != 10 and c != 13 { break; } + to = to - 1; + } + return s[from..to]; +} + +pub fn parse_int(s: []u8) -> ?i32 { + if s.n == 0 { return null; } + var value: i32 = 0; + var i: usize = 0; + var negative: bool = false; + if s[0] == 45 { negative = true; i = 1; } + if i >= s.n { return null; } + while i < s.n { + let c: u8 = s[i]; + if c < 48 or c > 57 { return null; } + value = value * 10 + ((c - 48) as i32); + i = i + 1; + } + if negative { return 0 - value; } + return value; +} diff --git a/fec/tests/exec/listuse.fe b/fec/tests/exec/listuse.fe new file mode 100644 index 0000000..2c0d703 --- /dev/null +++ b/fec/tests/exec/listuse.fe @@ -0,0 +1,42 @@ +// EXIT:0 +// OUTPUT:count 20 sum 190 balanced +unit listuse; +import std.io; +import std.fmt; +import std.list; +import std.sys; + +fn show(label: []u8, v: i32) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], v); + io.print(label); + io.print(" "); + io.print(buf[0..n]); + io.print(" "); +} + +fn build() -> !i32 { + var xs: list.List(i32) = try list.List(i32).with_capacity(2); + var i: i32 = 0; + while i < 20 { + try xs.push(i); + i = i + 1; + } + var sum: i32 = 0; + var k: usize = 0; + while k < xs.count() { + sum = sum + xs.at(k); + k = k + 1; + } + show("count", xs.count() as i32); + show("sum", sum); + return sum; +} + +fn main() -> i32 { + let sum: i32 = build() catch |e| { return 1; }; + if sum != 190 { return 2; } + if sys.allocs() != sys.frees() { io.print("leaked\n"); return 3; } + io.print("balanced\n"); + return 0; +} diff --git a/fec/tests/exec/strings.fe b/fec/tests/exec/strings.fe new file mode 100644 index 0000000..fb2afb9 --- /dev/null +++ b/fec/tests/exec/strings.fe @@ -0,0 +1,29 @@ +// EXIT:0 +// OUTPUT:ok 42 -7 +unit strings; +import std.io; +import std.str; +import std.fmt; + +fn show(v: i32) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], v); + io.print(" "); + io.print(buf[0..n]); +} + +fn main() -> i32 { + if not str.eq(str.trim(" hello "), "hello") { return 1; } + if not str.starts_with("ferro", "fer") { return 2; } + if str.find("abcdef", "cd") != 2 { return 3; } + if str.find("abcdef", "zz") != 6 { return 4; } + let a: i32 = str.parse_int("42") orelse 0; + let b: i32 = str.parse_int("-7") orelse 0; + let bad: i32 = str.parse_int("12x") orelse 0; + if bad != 0 { return 5; } + io.print("ok"); + show(a); + show(b); + io.print("\n"); + return 0; +} From 432d0731044b8ef607721bf53854af7bda11b984 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:45:19 +0900 Subject: [PATCH 149/184] =?UTF-8?q?lang:=20=EB=B0=B0=ED=83=80=20=EB=8C=80?= =?UTF-8?q?=EC=97=AC=EB=A5=BC=20=ED=98=B8=EC=B6=9C=EC=97=90=20=EB=84=98?= =?UTF-8?q?=EA=B8=B0=EB=8A=94=20=EA=B2=83=EC=9D=80=20=EC=9D=B4=EB=8F=99?= =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EB=8B=88=EB=9D=BC=20=EC=9E=AC=EB=8C=80?= =?UTF-8?q?=EC=97=AC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit &mut T 를 &mut T 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는 동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해서 &mut 가 사실상 쓸 수 없었다 -- 재귀 하강 파서를 쓰다가 걸렸다. 페이로드 없는 enum 은 이름 붙은 수라서 수로 읽을 수 있다. 반대 방향은 안 된다: 임의의 수는 변이가 아니다. calc 프로그램: 재귀 하강 수식 계산기. 우선순위, 괄호, 오류 전파. 1+2*3 = 7 (1+2)*3 = 9 2*(3+4)-5 = 9 10/3 = 3 1+ = error (1+2 = error --- SPEC.md | 1 + fec/src/check.c | 26 ++++++++- fec/tests/exec/calc.fe | 120 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 fec/tests/exec/calc.fe diff --git a/SPEC.md b/SPEC.md index b7058c0..6c8e6c8 100644 --- a/SPEC.md +++ b/SPEC.md @@ -110,6 +110,7 @@ and or not orelse - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. `let` 배열·공유 슬라이스에서는 `[]T`, `var` 배열·배타 슬라이스에서는 `[]mut T`가 생긴다. - `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. +- **배타 대여를 호출에 넘기는 것은 이동이 아니라 그 호출 동안의 재대여다.** `&mut T`를 `&mut T` 파라미터에, `[]mut T`를 `[]mut T` 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는 동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해져서 `&mut`가 사실상 쓸 수 없게 된다. - `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. - `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 C 표현은 없다. diff --git a/fec/src/check.c b/fec/src/check.c index 5da3711..6f238da 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -156,9 +156,27 @@ static int compatible(FeType *want, FeType *got, FeNode *value) value->text[0] != '\'' && value->text[0] != '"'; } +/* Does passing `arg` to a parameter of type `param` lend it rather than give + it away? An exclusive borrow handed to a call comes back when the call + returns, so it is not a move. */ +static int call_reborrows(const FeType *param, const FeType *arg) +{ + if (!param || !arg) return 0; + if (param->kind==FE_TYPE_REF && arg->kind==FE_TYPE_REF && + param->ref_mut && arg->ref_mut) return 1; + if (param->kind==FE_TYPE_SLICE && arg->kind==FE_TYPE_SLICE && + param->ref_mut && arg->ref_mut) return 1; + return 0; +} + static int explicit_castable(FeType *a, FeType *b) { if (!a || !b) return 0; + /* An enum without a payload is a number with names on it, so reading it + as one is a widening or narrowing and nothing more. The other direction + is not allowed: an arbitrary number is not a variant. */ + if (a->kind == FE_TYPE_ENUM && !a->fields && + (fe_type_is_integer(b) || b->kind == FE_TYPE_CHAR)) return 1; return (fe_type_is_integer(a) || a->kind == FE_TYPE_CHAR) && (fe_type_is_integer(b) || b->kind == FE_TYPE_CHAR); } @@ -2820,6 +2838,11 @@ static FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, } else if (b && a && b->kind==FE_TYPE_SLICE && !b->ref_mut && a->kind==FE_TYPE_SLICE && a->ref_mut) { /* Call-only []mut -> [] weakening is a temporary view. */ + } else if (call_reborrows(b, a)) { + /* Handing an exclusive borrow to a call lends it for the length of + that call and takes it back after: the caller cannot touch it + meanwhile, so nothing is aliased. Without this an exclusive + parameter could be passed onwards exactly once. */ } else mark_moved(s,arg,a); if (!compatible(b, a, arg) && !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && @@ -2916,7 +2939,8 @@ static FeType *check_call(FeCheckerState *s, FeNode *n) if (root && root->borrow_root) root=root->borrow_root; if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); } else if (!(b && a && b->kind==FE_TYPE_SLICE && - a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut)) + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut) && + !call_reborrows(b, a)) mark_moved(s,arg,arg->sem_type ? arg->sem_type : a); if (!fe_type_equal(b,a) && !m7_actual_compatible(b,a,arg) && !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && diff --git a/fec/tests/exec/calc.fe b/fec/tests/exec/calc.fe new file mode 100644 index 0000000..0f6b95a --- /dev/null +++ b/fec/tests/exec/calc.fe @@ -0,0 +1,120 @@ +// EXIT:0 +// OUTPUT:1+2*3 = 7 +// OUTPUT:(1+2)*3 = 9 +// OUTPUT:2*(3+4)-5 = 9 +// OUTPUT:10/3 = 3 +// OUTPUT:1+ = error +// OUTPUT:(1+2 = error +unit calc; +import std.io; +import std.fmt; + +// A recursive-descent evaluator over a byte slice. +// +// The position travels in a `&mut usize` rather than in a struct beside the +// text: a struct cannot hold a slice, because a slice is a borrowed view and +// R4 keeps borrows out of aggregate storage. Passing both is the honest way +// to say "this text, and how far we have read". + +fn done(src: []u8, at: usize) -> bool { return at >= src.n; } + +fn peek(src: []u8, at: usize) -> u8 { + if done(src, at) { return 0; } + return src[at]; +} + +fn skip_spaces(src: []u8, at: &mut usize) -> void { + while not done(src, at.^) { + if src[at.^] != 32 { break; } + at.^ = at.^ + 1; + } +} + +fn number(src: []u8, at: &mut usize) -> !i32 { + var value: i32 = 0; + var digits: usize = 0; + while not done(src, at.^) { + let c: u8 = src[at.^]; + if c < 48 or c > 57 { break; } + value = value * 10 + ((c - 48) as i32); + digits = digits + 1; + at.^ = at.^ + 1; + } + if digits == 0 { return error.BadNumber; } + return value; +} + +fn factor(src: []u8, at: &mut usize) -> !i32 { + skip_spaces(src, at); + if peek(src, at.^) == 40 { + at.^ = at.^ + 1; + let inner: i32 = try expr(src, at); + skip_spaces(src, at); + if peek(src, at.^) != 41 { return error.Unbalanced; } + at.^ = at.^ + 1; + return inner; + } + return number(src, at); +} + +fn term(src: []u8, at: &mut usize) -> !i32 { + var left: i32 = try factor(src, at); + while true { + skip_spaces(src, at); + let op: u8 = peek(src, at.^); + if op != 42 and op != 47 { break; } + at.^ = at.^ + 1; + let right: i32 = try factor(src, at); + if op == 42 { left = left * right; } + else { + if right == 0 { return error.DivideByZero; } + left = left / right; + } + } + return left; +} + +fn expr(src: []u8, at: &mut usize) -> !i32 { + var left: i32 = try term(src, at); + while true { + skip_spaces(src, at); + let op: u8 = peek(src, at.^); + if op != 43 and op != 45 { break; } + at.^ = at.^ + 1; + let right: i32 = try term(src, at); + if op == 43 { left = left + right; } + else { left = left - right; } + } + return left; +} + +fn evaluate(text: []u8) -> !i32 { + var at: usize = 0; + let value: i32 = try expr(text, &mut at); + skip_spaces(text, &mut at); + if not done(text, at) { return error.Trailing; } + return value; +} + +fn show(text: []u8) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + io.print(text); + io.print(" = "); + let value: i32 = evaluate(text) catch |e| { + io.print("error\n"); + return; + }; + let n: usize = fmt.fmt_i32(buf[..], value); + io.print(buf[0..n]); + io.print("\n"); +} + +fn main() -> i32 { + show("1+2*3"); + show("(1+2)*3"); + show("2*(3+4)-5"); + show("10/3"); + show("1+"); + show("(1+2"); + return 0; +} From 7f871a5d5f292975b48f26e582c9195c61076a39 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:46:47 +0900 Subject: [PATCH 150/184] =?UTF-8?q?tests:=20=EB=8B=A8=EC=96=B4=20=EB=B9=88?= =?UTF-8?q?=EB=8F=84=20=ED=94=84=EB=A1=9C=EA=B7=B8=EB=9E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컬렉션과 문자열을 함께 쓴다. 구조체를 담는 제네릭 리스트, 슬라이스 비교, 처음 본 순서 유지, 그리고 할당과 해제가 맞는지 확인. the 3 / cat 2 / sat 1 / distinct 5 / balanced run.py 209/209, exec.py 21/21. --- fec/tests/exec/wordfreq.fe | 82 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 fec/tests/exec/wordfreq.fe diff --git a/fec/tests/exec/wordfreq.fe b/fec/tests/exec/wordfreq.fe new file mode 100644 index 0000000..8088934 --- /dev/null +++ b/fec/tests/exec/wordfreq.fe @@ -0,0 +1,82 @@ +// EXIT:0 +// OUTPUT:the 3 +// OUTPUT:cat 2 +// OUTPUT:sat 1 +// OUTPUT:distinct 5 +// OUTPUT:balanced +unit wordfreq; +import std.io; +import std.fmt; +import std.str; +import std.list; +import std.sys; + +// Count how often each word appears, in the order the words were first seen. +// A word is a run of anything that is not a space. + +struct Word { + from: usize, + len: usize, + count: i32, +} + +fn report(text: []u8, w: Word) -> void { + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], w.count); + io.print(text[w.from..w.from + w.len]); + io.print(" "); + io.print(buf[0..n]); + io.print("\n"); +} + +fn tally(text: []u8) -> !i32 { + var words: list.List(Word) = try list.List(Word).with_capacity(4); + var at: usize = 0; + while at < text.n { + var stop: usize = at; + while stop < text.n { + if text[stop] == 32 { break; } + stop = stop + 1; + } + if stop > at { + let word: []u8 = text[at..stop]; + var seen: bool = false; + var i: usize = 0; + while i < words.count() { + let known: Word = words.at(i); + if str.eq(text[known.from..known.from + known.len], word) { + words.set(i, Word{ from: known.from, len: known.len, + count: known.count + 1 }); + seen = true; + break; + } + i = i + 1; + } + if not seen { + try words.push(Word{ from: at, len: stop - at, count: 1 }); + } + } + at = stop + 1; + } + var k: usize = 0; + while k < words.count() { + let w: Word = words.at(k); + if w.count > 1 or k < 3 { report(text, w); } + k = k + 1; + } + return words.count() as i32; +} + +fn main() -> i32 { + let distinct: i32 = tally("the cat sat on the mat the cat") catch |e| { + return 1; + }; + var buf: [16]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = fmt.fmt_i32(buf[..], distinct); + io.print("distinct "); + io.print(buf[0..n]); + io.print("\n"); + if sys.allocs() != sys.frees() { io.print("leaked\n"); return 2; } + io.print("balanced\n"); + return 0; +} From e6de12ca9468bd2be3bafb2098f2909d908f9e6d Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 06:48:06 +0900 Subject: [PATCH 151/184] =?UTF-8?q?docs:=20=EC=99=84=EC=84=B1=EB=90=9C=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EC=97=90=20=EB=A7=9E=EC=B6=B0=20AGENTS=20?= =?UTF-8?q?=EC=99=80=20TODO=20=EB=A5=BC=20=EB=8B=A4=EC=8B=9C=20=EC=93=B4?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파이프라인이 끝에서 끝까지 도는 상태다. 검증이 두 스위트로 나뉜다 -- 컴파일러가 프로그램에 대해 뭐라고 하는가, 그리고 컴파일된 프로그램이 실제로 무엇을 하는가. 전자만 보면 진단은 옳은데 코드가 안 나오는 상태를 놓친다. 세션 중에 완화한 이동 규칙 두 곳을 TODO 맨 위에 사람의 판단을 기다리는 항목으로 적었다. 규칙을 건드리기 전에 프로그램 쪽을 먼저 고쳐보라는 것도 작업 흐름에 넣었다. --- AGENTS.md | 74 +++++++++++++++++++++++------------------- TODO.md | 97 ++++++++++++++++++++++++------------------------------- 2 files changed, 84 insertions(+), 87 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3c8c5da..fc524e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,61 +1,69 @@ # doslang 작업 규칙 -DOS용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 문서는 `SPEC.md`이며 -이 파일은 그것을 구현할 때의 작업 규칙만 다룬다. +DOS/Windows용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. 규범 문서는 +`SPEC.md`이며 이 파일은 그것을 구현할 때의 작업 규칙만 다룬다. ## 문서 지도 | 파일 | 역할 | |---|---| -| `SPEC.md` | 언어 명세. 유일한 규범 문서. 구현 지시서와 표준 라이브러리 명세는 별도 문서 | -| `TODO.md` | 남은 작업, 미결 결정, 순서 | +| `SPEC.md` | 언어 명세. 유일한 규범 문서 | +| `IR.md` | 중간 표현. 프론트엔드와 기계 사이 | +| `TODO.md` | 남은 작업, 미결 결정, 정해진 것 | | `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 | -테스트 명령과 플래그는 다음 CLI로 확인한다. +## 파이프라인 -```powershell -uv run python tests/run.py --help +``` +.fe → fec → i386 asm → wasm → wlink → .exe + └ lexer parser resolve types own check (프론트엔드) + └ lower (IR) + └ x86 (백엔드) ``` -## 검증 규칙 +`wasm`과 `wlink`는 고정된 Open Watcom의 어셈블러와 링커다 (WebAssembly와 무관). +`SPEC.md` §1 철학 6: 링커와 오브젝트 포맷을 새로 만들지 않는다. -- 컴파일러는 현재 프런트엔드만 구현되어 있다. 범위: lexer/parser/types/own/check/resolve. -- 코드 생성기(백엔드/IR/`lowering`)는 아직 없다. -- 호스트 C 컴파일러는 구현/검증 대상이 아니다. 호스트는 편집, Git, 다운로드, 격리 - 작업공간 준비에만 쓴다. +## 검증 + +```powershell +uv run python tests/run.py # 컴파일러가 프로그램에 대해 뭐라고 하는가 +uv run python tests/exec.py # 컴파일된 프로그램이 실제로 무엇을 하는가 +uv run python tests/build.py <프로그램.fe> # 하나만 빌드해서 돌려보기 +``` + +- **두 스위트를 모두 통과해야 한다.** `run.py`만 보면 진단은 옳은데 코드가 안 나오는 + 상태를 놓친다. 보고만 되고 방출되지 않는 경계 검사가 그 예다. - 완료하려는 기능을 직접 검사하는 fixture가 통과해야 한다. 테스트가 증명하지 않는 기능은 완료로 처리하지 않는다. -- `uv run python tests/run.py` 가 유일한 검증 엔트리다. 툴체인은 `.dosboxx/watcom`에 - 고정되어 있고, 없으면 오류로 멈춘다. - 거부를 기대하는 fixture는 첫 줄에 `// ERROR:<줄>:<문구>` 마커를 둔다. 마커가 없으면 - "거부되기만 하면 통과"라 검증이 약하다. -- 과거 VM 이미지나 호스트에 남은 바이너리는 완료 근거로 쓰지 않는다. -- 실행해야만 검증되는 fixture는 `fec/tests/pending-backend/`에 두고 러너가 건너뛴다. + 파일 이름이 기대값이 된다 — `bad`로 시작하면 거부, 아니면 통과. +- 실행 프로그램은 첫 줄들에 `// EXIT:<코드>`, `// OUTPUT:<문구>`, `// NOCHECKS:<코드>`를 + 둔다. 마지막 것은 `--no-checks`로 다시 빌드해서 다른 결과를 요구한다. +- 툴체인은 `.dosboxx/watcom`에 고정되어 있고, 없으면 오류로 멈춘다. -## 빌드 함정 +## 함정 -- [백엔드 복귀 시 유효] 컴파일러는 16비트 large model로 빌드한다. small model은 - 메모리 부족으로 실패한다. -- [백엔드 복귀 시 유효] 링크는 `*.obj` 와일드카드로 한다. DOS 명령줄 길이 제한 때문에 - 오브젝트를 개별 열거할 수 없다. -- [백엔드 복귀 시 유효] M4 Watcom 테스트는 `-wx -wcd=202`를 쓴다. 생성 C의 보수적 미 - 사용 helper 때문에 W202만 끄고 나머지 경고는 오류로 유지한다. -- [백엔드 복귀 시 유효] fixture는 DOS 8.3 이름으로 실행한다. 긴 이름은 registry에서 - 명시적으로 줄인다. -- [백엔드 복귀 시 유효] `R:`은 읽기 전용 저장소, `W:`은 읽기 전용 Watcom이다. - 빌드 산출물은 반드시 임시 `C:\FEC`에 쓴다. -- [백엔드 복귀 시 유효] 실패 분석에는 임시 작업공간 보존이 필요하다. 그 플래그는 - 런처와 함께 사라졌으므로 다시 만들어야 한다. +- 표준 라이브러리는 프로그램이 아니라 컴파일러 옆에 있다. `--std=<디렉터리>`로 + 넘기며, 그 디렉터리 안에 `std/`가 있어야 한다. +- 유닛 경로의 각 segment는 소문자로 시작하고 `a-z0-9_`만, **최대 8자**다. + 파일 경로와 정확히 대응한다 (`std.io` ↔ `/std/io.fe`). +- `extern "c" fn`은 이름을 그대로 쓴다. 나머지는 `fe_<유닛>_<이름>`으로 맹글링하며 + 어셈블러가 받지 않는 문자는 밑줄이 된다. +- 슬라이스 배치(포인터 다음 길이)와 wrapper 페이로드 위치는 각각 한 군데에만 + 적혀 있다. 두 군데가 되면 어긋난다. ## 작업 흐름 - 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신한다. 구현이 명세와 다르면 둘 중 하나가 틀린 것이므로 그 자리에서 결론을 낸다. +- 언어 규칙을 완화하려거든 먼저 프로그램 쪽을 고쳐본다. 규칙이 진짜 언어를 못 쓰게 + 만들 때만 규칙을 건드리고, 무엇을 왜 바꿨는지 `TODO.md`에 남겨 사람이 판단하게 + 한다. - 코드는 컴파일러 단계로 나눈다. 마일스톤 단위 분할은 폐기했다. -- 검증된 단위마다 커밋한다. 푸시는 요청받았을 때만 한다. -- primary 브랜치는 `master`다. +- 검증된 단위마다 커밋한다. primary 브랜치는 `master`다. - `.dosboxx/`의 다운로드, 실행 작업공간, 로그는 커밋하지 않는다. ## 현재 상태 -`uv run python tests/run.py` 의 통과 수가 현재 상태다. 남은 작업은 `TODO.md`에 있다. +두 스위트의 통과 수가 현재 상태다. 남은 작업은 `TODO.md`에 있다. diff --git a/TODO.md b/TODO.md index 0d5f8d3..0026e7c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,9 +1,27 @@ # TODO -현재: **188/188** (`uv run python tests/run.py`) +``` +uv run python tests/run.py 209/209 컴파일러가 프로그램에 대해 뭐라고 하는가 +uv run python tests/exec.py 21/21 컴파일된 프로그램이 실제로 무엇을 하는가 +``` -프론트엔드는 끝났다. 목표는 **i386 백엔드 + stdlib 으로 Windows 11 용 컴파일러를 -완성하는 것**이다. 아직 실행된 Ferro 프로그램은 하나도 없다. +파이프라인이 끝에서 끝까지 돈다. + +``` +.fe → fec → i386 asm → wasm → wlink → .exe → Windows 11 +``` + +--- + +## 네 결정을 기다리는 것 + +세션 중에 **이동 규칙 두 곳을 완화**했다. 둘 다 R4(참조는 집합 저장소에 +못 들어감)가 아니라 이동 쪽이고, 되돌릴 수 있다. + +| | 무엇 | 왜 | 대안 | +|---|---|---|---| +| 1 | `&mut T`를 `&mut T` 파라미터에 넘기는 것은 이동이 아니라 **호출 동안의 재대여** | 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번뿐이라 `&mut`가 사실상 죽는다. SPEC §4.2에 이미 있던 "호출 인자 위치에서만" 재대여를 같은 종류끼리로 넓힌 것 | 되돌리면 재귀 하강 파서 같은 것을 못 쓴다 | +| 2 | 자기 `drop` 안에서는 필드를 꺼낼 수 있다 (R7 예외) | 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다. `mem.replace`로 우회하려면 유효한 대체값이 필요한데 그런 것이 없다 | `drop(self: Self)`로 값을 소비하게 바꾸면 R7을 안 건드려도 된다 | --- @@ -11,72 +29,43 @@ | # | 일 | 규모 | 비고 | |---|---|---|---| -| 1 | **IR 정의** | 중 | 3-address, 기본 블록, 함수 단위. 명령 12 + 종결자 4 | -| 2 | **lowering** | 대 | `try`/`catch`/`orelse`/`defer`/drop/`for`/메서드/경계검사/옵셔널·에러유니온 구성/제네릭 인스턴스 전개. **프로젝트 무게중심** | -| 3 | i386 백엔드 | 대 | IR → x86 asm → `wasm` → `wlink` → PE. 명령 선택, 레지스터 할당, 호출 규약 | -| 4 | 런타임 | 소~중 | 시작 스텁, `fe_trap(reason, file, line)`, 할당, 종료 | -| 5 | `pending-backend/` 4개 복귀 | 소 | bounds trap, `--no-checks` 차등, 소유권 drop. **처음으로 실행이 검증됨** | -| 6 | stdlib 명세 | 중 | SPEC §10 이 플레이스홀더. 시그니처·오류·경계 동작 미정 | -| 7 | stdlib 구현 | 대 | `core` `mem` `fmt` `io` `sys`, 그리고 제네릭이 생겼으니 `list` `map` | +| 1 | 레지스터 할당 | 중 | 지금은 임시값마다 스택 슬롯이다. IR이 "임시값은 블록을 넘지 않는다"라서 블록 단위 할당기면 충분하다 | +| 2 | `@print` 전개 | 소~중 | 지금은 `io.print` + `fmt.fmt_*`를 손으로 부른다. SPEC §6.3.1은 컴파일 단계 전개를 요구한다 | +| 3 | `std.map` | 중 | `std.list`는 있다. 해시는 아직 | +| 4 | `std.io` 읽기 | 소 | `Reader`, `io.read`. 지금은 쓰기만 | +| 5 | `match` 페이로드 | 중 | 태그 비교는 된다. 페이로드 바인딩은 아직 | +| 6 | `if let` | 소 | 프론트엔드는 검사한다. lowering이 아직 | +| 7 | 셀프호스팅 | 대 | `fec`을 Ferro로. 여기까지 오면 언어가 자기 무게를 견딘다는 증거 | -## 그 이후 +## 미뤄둔 것 -| | | | -|---|---|---| -| 셀프호스팅 | 대 | `fec` 을 Ferro 로. 여기까지 오면 언어가 자기 무게를 견딘다는 증거 | -| 인터럽트·공유 상태 | 중 | `interrupt` `shared` `atomic` `critical` — 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | -| 다른 32비트 타깃 | 대 | m68k / ARM / MIPS / RV32. IR 결정은 전부 ISA 중립이라 백엔드만 붙이면 됨 | +| | | +|---|---| +| 인터럽트·공유 상태 | `interrupt` `shared` `atomic` `critical` — 파싱만 되고 의미 없음. SPEC §11에서 v0.2 | +| 다른 32비트 타깃 | m68k / ARM / MIPS / RV32. IR 결정은 전부 ISA 중립이라 백엔드만 붙이면 된다 | +| 엔디안 | 리틀엔디안 가정. `packed struct`가 바이트 배치를 약속하므로 빅엔디안 타깃이 생기면 타깃 파라미터가 된다 | +| fixture 이름 122개 | DOS 8.3 시절 잔재. 마커가 다 붙어서 `bad`/`ok` 접두사는 더 이상 기대값이 아니다 | --- -## 미결 결정 - -| | 내용 | 언제 | -|---|---|---| -| 덩어리 전달 규약 | 정함 (전부 주소로). 3번 착수 전 재확인 | 3 전 | -| `extern` C 상호운용 | 위 규약이 C ABI 와 안 맞음. 경계 변환 필요 | 3 전 | -| 엔디안 | 리틀엔디안 가정. `packed struct` 가 바이트 배치를 약속하므로, 빅엔디안 타깃(m68k·MIPS·POWER)이 생기면 타깃 파라미터가 된다 | 다른 타깃 전 | -| stdlib 명세 | SPEC §10 이 플레이스홀더 | 6 | - ## 정해진 것 | | | |---|---| | 타깃 | **i386 하나.** 세그먼트 없음, `far` 영구 제외 (SPEC §2) | -| `usize`/`isize` | **타깃의 포인터 폭.** 비트 수를 약속하지 않으므로 64비트 문이 닫히지 않음 | -| 제네릭 | 모노모피제이션. 순수 프론트엔드 기능이라 IR 에 제네릭 개념이 없음 | -| 덩어리 전달 | 전부 주소로. 크기 임계값 없음 — ISA 마다 다른 구조체 전달 ABI 를 피해감 | +| `usize`/`isize` | **타깃의 포인터 폭.** 비트 수를 약속하지 않아 64비트 문이 닫히지 않음 | +| 제네릭 | 모노모피제이션. 순수 프론트엔드 기능이라 IR에 제네릭 개념이 없음 | +| 덩어리 전달 | 전부 주소로. 크기 임계값 없음 — ISA마다 다른 구조체 전달 ABI를 피해감 | | 트랩 | `trap ` → `fe_trap(reason, UNIT_FILE, line)`. 유닛당 파일 문자열 하나 | +| 슬라이스 배치 | 포인터 다음 길이. 오프셋은 `lower.c` 한 군데에만 있다 | +| 오류 코드 | `error.Name`을 빌드 전체에서 모아 철자 순으로 1부터. 빌드 순서가 결과를 안 바꿈 | | 셀프호스팅 | 640KB 목표 아님. 32비트 보호모드에서 돈다 (SPEC §2.1) | --- -## 순서 - -``` -1 → 2 IR + lowering -3 → 4 → 5 i386 백엔드, 런타임, 실행 검증 복귀 ← 여기서 처음 실행됨 -6 → 7 stdlib -``` - ---- - ## 위임 정책 -**위임 가능** — 결과가 자명하고 판단이 없는 것 +**위임 가능** — 결과가 자명하고 판단이 없는 것. 파일 목록을 **명시적으로 열거**하고 +기준선 두 숫자를 주고 **변하면 안 된다**고 못박을 것. -- fixture 이름 개선, README 갱신, 문서 정합 -- 파일 목록을 **명시적으로 열거해서** 줄 것. "찾아서 하라" 는 약함 -- 기준선 두 숫자(`N/188`, `M pin`)를 주고 **변하면 안 된다**고 못박을 것 - -**위임 불가** — 언어 의미론 판단이 섞인 것 - -- 마커 판정, IR/lowering/백엔드, stdlib 명세 - ---- - -## 미뤄둔 정리 작업 - -fixture 122개 이름이 아직 DOS 8.3 시절 잔재라 무엇을 검사하는지 이름만으로는 -알 수 없다. 마커가 전부 붙었으므로 `bad`/`ok` 접두사는 이제 기대값이 아니고, -자유롭게 이름을 지을 수 있다. 급하지 않다. +**위임 불가** — 언어 의미론 판단이 섞인 것. 마커 판정, IR/lowering/백엔드, stdlib 명세. From 390345ef85f26b96307b42aa213dbdfb76310295 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:04:35 +0900 Subject: [PATCH 152/184] =?UTF-8?q?refactor:=20check.c=20=EC=99=80=20lower?= =?UTF-8?q?.c=20=EB=A5=BC=20=EC=82=AC=EB=9E=8C=EC=9D=B4=20=EB=A8=B8?= =?UTF-8?q?=EB=A6=AC=EC=97=90=20=EB=8B=B4=EC=9D=84=20=ED=81=AC=EA=B8=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=82=98=EB=88=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check.c 3,937 줄, lower.c 1,913 줄이었다. 가장 큰 파일이 978 줄이 됐다. check.c 679 스코프·심볼·흐름·소유권 접착 checkexp.c 739 포매팅 검사와 표현식 checkstm.c 635 문장, 함수, 메서드 checkgen.c 618 제네릭 실체화 checkcal.c 978 유닛 경계 호출과 옵셔널/에러 유니온 checkpro.c 184 선언 패스와 프로그램 lower.c 567 타입·슬롯·지역·블록·mem.* lowerprn.c 238 포매팅 빌트인 전개 lowerexp.c 468 표현식 lowerstm.c 543 문장·함수·프로그램 줄 범위로 잘랐다. 주제별로 묶는 것보다 정확한데, 한 줄도 잃거나 겹치지 않기 때문이다. 파일 순서가 이미 단계를 따라가서 경계가 실제 이음매에 떨어진다. 모든 정의가 static 을 잃고 비공개 헤더에 프로토타입을 갖는다. 대안 -- static 을 유지하고 #include 로 텍스트만 나누는 것 -- 은 결합을 보여주는 대신 숨긴다. 두 스위트 그대로: 209/209, 21/21. --- fec/rt/start.asm | 90 +- fec/src/check.c | 3354 +------------------------------------------- fec/src/checkcal.c | 978 +++++++++++++ fec/src/checkexp.c | 739 ++++++++++ fec/src/checkgen.c | 618 ++++++++ fec/src/checkpri.h | 252 ++++ fec/src/checkpro.c | 182 +++ fec/src/checkstm.c | 635 +++++++++ fec/src/lower.c | 1271 +---------------- fec/src/lowerexp.c | 468 +++++++ fec/src/lowerpri.h | 153 ++ fec/src/lowerprn.c | 236 ++++ fec/src/lowerstm.c | 543 +++++++ tests/run.py | 3 +- 14 files changed, 4982 insertions(+), 4540 deletions(-) create mode 100644 fec/src/checkcal.c create mode 100644 fec/src/checkexp.c create mode 100644 fec/src/checkgen.c create mode 100644 fec/src/checkpri.h create mode 100644 fec/src/checkpro.c create mode 100644 fec/src/checkstm.c create mode 100644 fec/src/lowerexp.c create mode 100644 fec/src/lowerpri.h create mode 100644 fec/src/lowerprn.c create mode 100644 fec/src/lowerstm.c diff --git a/fec/rt/start.asm b/fec/rt/start.asm index 45ead57..c2726e6 100644 --- a/fec/rt/start.asm +++ b/fec/rt/start.asm @@ -26,7 +26,7 @@ prefix db 'ferro: ',0 at_word db ' at ',0 colon db ':',0 newline db 13,10,0 -numbuf db 16 dup(0) +numbuf db 24 dup(0) written dd 0 allocs dd 0 frees dd 0 @@ -210,6 +210,94 @@ fe_rt_frees proc near ret fe_rt_frees endp +; fe_rt_write_int(handle, value, is_unsigned) -- decimal, with a sign when +; the value is negative and signed was asked for. +public fe_rt_write_int +fe_rt_write_int proc near + push ebp + mov ebp, esp + push ebx + push esi + push edi + mov edi, offset numbuf + 15 + mov byte ptr [edi], 0 + mov eax, [ebp+12] + xor ebx, ebx ; ebx = 1 when a '-' is needed + cmp dword ptr [ebp+16], 0 + jne int_digits + test eax, eax + jge int_digits + neg eax + mov ebx, 1 +int_digits: + mov ecx, 10 +int_loop: + xor edx, edx + div ecx + add dl, '0' + dec edi + mov [edi], dl + test eax, eax + jnz int_loop + test ebx, ebx + je int_write + dec edi + mov byte ptr [edi], '-' +int_write: + mov esi, offset numbuf + 15 + sub esi, edi + push esi + push edi + push dword ptr [ebp+8] + call fe_rt_write + add esp, 12 + pop edi + pop esi + pop ebx + mov esp, ebp + pop ebp + ret +fe_rt_write_int endp + +; fe_rt_write_hex(handle, value) +public fe_rt_write_hex +fe_rt_write_hex proc near + push ebp + mov ebp, esp + push ebx + push esi + push edi + mov edi, offset numbuf + 15 + mov byte ptr [edi], 0 + mov eax, [ebp+12] +hex_loop: + mov edx, eax + and edx, 15 + cmp dl, 10 + jb hex_digit + add dl, 'a' - 10 - '0' +hex_digit: + add dl, '0' + dec edi + mov [edi], dl + shr eax, 4 + test eax, eax + jnz hex_loop + mov esi, offset numbuf + 15 + sub esi, edi + push esi + push edi + push dword ptr [ebp+8] + call fe_rt_write + add esp, 12 + pop edi + pop esi + pop ebx + mov esp, ebp + pop ebp + ret +fe_rt_write_hex endp + ; fe_rt_exit(code) -- never returns public fe_rt_exit fe_rt_exit proc near diff --git a/fec/src/check.c b/fec/src/check.c index 6f238da..ea1a250 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -1,77 +1,28 @@ -#include "check.h" -#include "m7.h" -#include +#include "checkpri.h" -#define FE_M7_FLOW_CAP 64U -#include "own.h" -#include -#include - -typedef struct FeSym FeSym; -/* FeScope is forward declared in check.h. */ - -struct FeSym { - const char *name; - char *cname; - FeType *type; - FeNode *fn; - int mutable; - int initialized; - int moved; - FeNode *decl; - /* M6 ownership is tracked at the root local/parameter. A reference - binding remembers that root so releasing the binding's last use can - release the root borrow without a separate alias engine. */ - FeOwnState own; - FeSym *borrow_root; - int borrow_mut; - int borrow_defer; - FeScope *owner; -}; - -struct FeScope { - FeScope *parent; - FeSym *items; - unsigned count; - unsigned capacity; -}; - -static FeSym *find_symbol(FeScope *scope, const char *name); - -typedef struct FeCheckerState { - FeCheck *c; - FeScope *scope; - FeScope *globals; - FeType *ret; - unsigned loop_depth; - unsigned defer_depth; - FeOwnLiveness liveness; - FeNode *fn_node; -} FeCheckerState; - -static FeType *unknown(FeCheck *c) +FeType *unknown(FeCheck *c) { return fe_type_intern(&c->types, ""); } -static void err(FeCheck *c, FeLoc loc, const char *msg) +void err(FeCheck *c, FeLoc loc, const char *msg) { fe_diag_error(c->diags, loc, msg); } /* Only numbers and characters have an order (SPEC 6.2). */ -static int ordered_type(const FeType *t) +int ordered_type(const FeType *t) { return t && (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_CHAR); } -static int known(FeType *t) +int known(FeType *t) { return t && t->kind != FE_TYPE_UNKNOWN && t->kind != FE_TYPE_ERROR; } /* Is this a projection of `self` inside that type's own `drop`? */ -static int in_own_drop(FeCheckerState *s, FeNode *n) +int in_own_drop(FeCheckerState *s, FeNode *n) { FeNode *base; if (!s->fn_node || !s->fn_node->text || strcmp(s->fn_node->text,"drop")!=0) @@ -83,7 +34,7 @@ static int in_own_drop(FeCheckerState *s, FeNode *n) strcmp(base->text,"self")==0; } -static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) +void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) { FeSym *sym=0; /* Inside a type's own `drop` the object is going away, so taking a field @@ -128,7 +79,7 @@ static void mark_moved(FeCheckerState *s, FeNode *n, FeType *t) n,t,s->defer_depth != 0); } -static int compatible(FeType *want, FeType *got, FeNode *value) +int compatible(FeType *want, FeType *got, FeNode *value) { FeNode *item; unsigned long count; @@ -159,7 +110,7 @@ static int compatible(FeType *want, FeType *got, FeNode *value) /* Does passing `arg` to a parameter of type `param` lend it rather than give it away? An exclusive borrow handed to a call comes back when the call returns, so it is not a move. */ -static int call_reborrows(const FeType *param, const FeType *arg) +int call_reborrows(const FeType *param, const FeType *arg) { if (!param || !arg) return 0; if (param->kind==FE_TYPE_REF && arg->kind==FE_TYPE_REF && @@ -169,7 +120,7 @@ static int call_reborrows(const FeType *param, const FeType *arg) return 0; } -static int explicit_castable(FeType *a, FeType *b) +int explicit_castable(FeType *a, FeType *b) { if (!a || !b) return 0; /* An enum without a payload is a number with names on it, so reading it @@ -181,7 +132,7 @@ static int explicit_castable(FeType *a, FeType *b) (fe_type_is_integer(b) || b->kind == FE_TYPE_CHAR); } -static FeType *node_type(FeCheck *c, FeNode *n) +FeType *node_type(FeCheck *c, FeNode *n) { FeType *t; if (!n) return unknown(c); @@ -193,7 +144,7 @@ static FeType *node_type(FeCheck *c, FeNode *n) /* A link-visible name. A unit path has dots in it and a generic instance has brackets and commas, none of which an assembler will accept, so everything outside the portable identifier set becomes an underscore. */ -static char *unit_cname(FeCheck *c, const char *name) +char *unit_cname(FeCheck *c, const char *name) { char *u; char *p; @@ -217,7 +168,7 @@ static char *unit_cname(FeCheck *c, const char *name) return p; } -static char *local_cname(FeCheck *c, const char *name) +char *local_cname(FeCheck *c, const char *name) { char number[24]; char *p; @@ -234,7 +185,7 @@ static char *local_cname(FeCheck *c, const char *name) return p; } -static FeScope *scope_new(FeCheckerState *s, FeScope *parent) +FeScope *scope_new(FeCheckerState *s, FeScope *parent) { FeScope *scope; scope = (FeScope *)fe_arena_alloc(&s->c->arena, sizeof(FeScope)); @@ -249,7 +200,7 @@ static FeScope *scope_new(FeCheckerState *s, FeScope *parent) return scope; } -static FeSym *find_current(FeScope *scope, const char *name) +FeSym *find_current(FeScope *scope, const char *name) { unsigned i; if (!scope) return 0; @@ -259,7 +210,7 @@ static FeSym *find_current(FeScope *scope, const char *name) return 0; } -static FeSym *find_symbol(FeScope *scope, const char *name) +FeSym *find_symbol(FeScope *scope, const char *name) { FeSym *sym; while (scope) { @@ -270,7 +221,7 @@ static FeSym *find_symbol(FeScope *scope, const char *name) return 0; } -static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, +FeSym *add_symbol(FeCheckerState *s, FeScope *scope, const char *name, FeType *type, FeNode *fn, int mutable, int initialized, char *cname, FeNode *decl) @@ -322,7 +273,7 @@ static FeSym *add_symbol(FeCheckerState *s, FeScope *scope, /* Make `unit` the one being checked. Types intern against its name, cnames are built from it, and diagnostics quote its source rather than whichever file happened to be parsed last. */ -static void enter_unit(FeCheck *c, unsigned index) +void enter_unit(FeCheck *c, unsigned index) { FeUnit *u = &c->build->units[index]; c->unit = u; @@ -331,13 +282,7 @@ static void enter_unit(FeCheck *c, unsigned index) fe_diags_source(c->diags, u->source, u->size); } -/* The type bindings in force, saved across a nested instantiation. */ -typedef struct FeBindSave { - FeTypeBind params[FE_TYPE_PARAM_MAX]; - unsigned count; -} FeBindSave; -static FeType *instantiate_type_node(void *owner, const FeNode *node); void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, unsigned pointer_bits, int no_checks) @@ -367,7 +312,7 @@ void fe_check_destroy(FeCheck *c) fe_arena_destroy(&c->arena); } -static unsigned unit_index(FeCheck *c, const FeUnit *u) +unsigned unit_index(FeCheck *c, const FeUnit *u) { return (unsigned)(u - c->build->units); } @@ -376,7 +321,7 @@ static unsigned unit_index(FeCheck *c, const FeUnit *u) unit it names. A local of the same spelling wins -- shadowing a binding is legal and means the local -- so this only answers when the base name is not otherwise in scope. */ -static FeUnit *binding_unit(FeCheckerState *s, FeNode *base) +FeUnit *binding_unit(FeCheckerState *s, FeNode *base) { if (!base || base->kind!=FE_N_IDENT || !base->text) return 0; if (!s->c->build || !s->c->unit) return 0; @@ -385,12 +330,12 @@ static FeUnit *binding_unit(FeCheckerState *s, FeNode *base) } /* SPEC 8.2: a declaration is visible outside its unit only with `pub`. */ -static int decl_is_public(const FeNode *decl) +int decl_is_public(const FeNode *decl) { return decl && (decl->flags & FE_NODE_PUB)!=0; } -static FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name) +FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name) { if (!u || !name) return 0; return find_current(c->unit_scope[unit_index(c,u)],name); @@ -399,7 +344,7 @@ static FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name) /* A type another unit declares, or null if it declares no such type. Interning is keyed on the declaring unit, so this cannot collide with a same-named type here. */ -static FeType *unit_type(FeCheck *c, FeUnit *u, const char *name) +FeType *unit_type(FeCheck *c, FeUnit *u, const char *name) { FeType *t; if (!u || !name) return 0; @@ -411,7 +356,7 @@ static FeType *unit_type(FeCheck *c, FeUnit *u, const char *name) /* The AST declaration of a type another unit declares, for its visibility and for its methods. */ -static FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name) +FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name) { FeNode *n; (void)c; @@ -425,7 +370,7 @@ static FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name) /* Resolve a type written in another unit's source. Names in a signature mean what they meant where the signature was written, not where it is called. */ -static FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node) +FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node) { const char *save=c->types.unit_name; FeType *t; @@ -435,9 +380,8 @@ static FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node) return t; } -static FeType *check_expr(FeCheckerState *s, FeNode *n); -static FeNode *find_method(FeCheck *c, FeType *owner, const char *name) +FeNode *find_method(FeCheck *c, FeType *owner, const char *name) { FeNode *decl; FeNode *method; @@ -457,7 +401,7 @@ static FeNode *find_method(FeCheck *c, FeType *owner, const char *name) return 0; } -static FeType *method_type(FeCheck *c, FeNode *node, FeType *owner) +FeType *method_type(FeCheck *c, FeNode *node, FeType *owner) { if(node && node->kind==FE_N_TYPE && node->text && strcmp(node->text,"Self")==0) return owner; @@ -467,57 +411,9 @@ static FeType *method_type(FeCheck *c, FeNode *node, FeType *owner) return fe_type_ref(&c->types,owner,strcmp(node->text,"&mut")==0); return node_type(c,node); } -static void check_match(FeCheckerState *s, FeNode *n); -static void check_stmt(FeCheckerState *s, FeNode *n); -static FeType *check_expr_core(FeCheckerState *s, FeNode *n); -static void check_stmt_core(FeCheckerState *s, FeNode *n); -static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, - FeType *base_in); -static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read); -static FeType *check_call(FeCheckerState *s, FeNode *n); -static FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, - const char *home, unsigned skip); -static int is_error_set_member(FeCheckerState *s, FeNode *n); -static void check_fn(FeCheck *c, FeNode *n, FeScope *globals); -static void check_method(FeCheck *c, FeNode *n, FeScope *globals, FeType *owner); -static char *unit_cname(FeCheck *c, const char *name); -static unsigned unit_index(FeCheck *c, const FeUnit *u); -static FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name); -static FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, - FeUnit *home); -static FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok); -static FeUnit *current_unit(FeCheck *c); -static int decl_is_generic(const FeNode *decl); -static void check_generic_params(FeCheck *c, FeNode *decl); -static int comptime_condition(FeCheckerState *s, FeNode *n, int *out); -static FeType *check_static_method_call(FeCheckerState *s, FeNode *n, - FeType *owner, FeNode *method); -static FeNode *type_method(FeType *t, const char *name); -static int method_is_static(const FeNode *method); -static int const_names_type(FeCheckerState *s, FeNode *n); -static void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t); -static void pop_bindings(FeCheck *c, const FeBindSave *save); -static void bind_self(FeCheck *c, FeType *owner); -static void instance_key(char *out, const char *unit, const char *name, - FeType **args, unsigned count); -static int instance_record(FeCheck *c, const char *key, FeLoc loc, - FeNode *decl, FeUnit *home, FeType *owner); -static const char *instance_cname(FeCheck *c, const char *key); -static int instance_descend(FeCheck *c, FeLoc loc); -static void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, - FeType *owner, FeBindSave *bindings, FeLoc site); -static void check_instance_method(FeCheckerState *s, FeType *owner, - FeNode *method, FeLoc site, FeNode *call); -typedef struct FeFlowSlot { - FeSym *sym; - int moved; - int initialized; - int own_move; - int own_initialized; -} FeFlowSlot; -static unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap) +unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap) { unsigned count=0; unsigned i; @@ -534,7 +430,7 @@ static unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap) return count; } -static void flow_restore(FeFlowSlot *slots, unsigned count) +void flow_restore(FeFlowSlot *slots, unsigned count) { unsigned i; for (i=0; iscope,place.root->text ? place.root->text : ""); } -static int own_is_global(FeCheckerState *s, FeSym *sym) +int own_is_global(FeCheckerState *s, FeSym *sym) { FeScope *p; if (!s || !sym) return 0; @@ -575,7 +471,7 @@ static int own_is_global(FeCheckerState *s, FeSym *sym) return 0; } -static void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) +void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) { FeSym *root=own_root_symbol(s,expr); if (!root) return; @@ -595,7 +491,7 @@ static void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) expr->loc); } -static void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) +void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) { FeSym *root; if (!expr || expr->kind!=FE_N_UNARY || !expr->text) return; @@ -608,7 +504,7 @@ static void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) /* Return-reference provenance is represented at call sites by retaining a borrow of the unique reference-derived argument (or method receiver). */ -static FeSym *own_derived_call_root(FeCheckerState *s, FeNode *call) +FeSym *own_derived_call_root(FeCheckerState *s, FeNode *call) { FeNode *param; FeNode *arg; @@ -633,7 +529,7 @@ static FeSym *own_derived_call_root(FeCheckerState *s, FeNode *call) return refs==1 ? own_root_symbol(s,source) : 0; } -static void own_bind_derived_call(FeCheckerState *s, FeSym *binding, +void own_bind_derived_call(FeCheckerState *s, FeSym *binding, FeNode *value) { FeSym *root; @@ -649,7 +545,7 @@ static void own_bind_derived_call(FeCheckerState *s, FeSym *binding, binding->borrow_mut=value->sem_type->kind==FE_TYPE_REF && value->sem_type->ref_mut; } -static int own_stmt_uses(FeNode *node, const char *name) +int own_stmt_uses(FeNode *node, const char *name) { FeNode *x; if (!node || !name) return 0; @@ -662,7 +558,7 @@ static int own_stmt_uses(FeNode *node, const char *name) return 0; } -static int own_defer_uses(FeNode *node, const char *name) +int own_defer_uses(FeNode *node, const char *name) { FeNode *x; if (!node) return 0; @@ -674,7 +570,7 @@ static int own_defer_uses(FeNode *node, const char *name) return 0; } -static int own_contains_node(FeNode *node, FeNode *needle) +int own_contains_node(FeNode *node, FeNode *needle) { FeNode *x; if (!node || !needle) return 0; @@ -687,7 +583,7 @@ static int own_contains_node(FeNode *node, FeNode *needle) return 0; } -static void own_release_after_stmt(FeCheckerState *s, FeScope *scope, +void own_release_after_stmt(FeCheckerState *s, FeScope *scope, FeNode *stmt, int scope_end) { unsigned i; @@ -709,14 +605,14 @@ static void own_release_after_stmt(FeCheckerState *s, FeScope *scope, /* Full borrow snapshots live in the AST arena, rather than on the 16-bit compiler stack. The compact FeFlowSlot arrays retain the pre-M6 move and initialization flow handling. */ -static FeOwnState *flow_own_new(FeCheckerState *s, unsigned count) +FeOwnState *flow_own_new(FeCheckerState *s, unsigned count) { if (!s || !count) return 0; return (FeOwnState *)fe_arena_alloc(&s->c->arena, count*sizeof(FeOwnState)); } -static void flow_own_capture(FeFlowSlot *slots, FeOwnState *states, +void flow_own_capture(FeFlowSlot *slots, FeOwnState *states, unsigned count) { unsigned i; @@ -724,7 +620,7 @@ static void flow_own_capture(FeFlowSlot *slots, FeOwnState *states, for (i=0;iown; } -static void flow_own_restore(FeFlowSlot *slots, FeOwnState *states, +void flow_own_restore(FeFlowSlot *slots, FeOwnState *states, unsigned count) { unsigned i; @@ -732,7 +628,7 @@ static void flow_own_restore(FeFlowSlot *slots, FeOwnState *states, for (i=0;iown=states[i]; } -static void flow_own_merge(FeFlowSlot *slots, FeOwnState *left, +void flow_own_merge(FeFlowSlot *slots, FeOwnState *left, FeOwnState *right, unsigned count) { unsigned i; @@ -741,19 +637,15 @@ static void flow_own_merge(FeFlowSlot *slots, FeOwnState *left, slots[i].sym->own=fe_own_merge_state(left[i],right[i]); } -typedef struct FeFlowBorrow { - FeSym *root; - int mutable; -} FeFlowBorrow; -static FeFlowBorrow *flow_borrow_new(FeCheckerState *s, unsigned count) +FeFlowBorrow *flow_borrow_new(FeCheckerState *s, unsigned count) { if (!s || !count) return 0; return (FeFlowBorrow *)fe_arena_alloc(&s->c->arena, count*sizeof(FeFlowBorrow)); } -static void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, +void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, unsigned count) { unsigned i; @@ -764,7 +656,7 @@ static void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, } } -static void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, +void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, unsigned count) { unsigned i; @@ -775,7 +667,7 @@ static void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, } } -static void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, +void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, FeFlowBorrow *right, unsigned count) { unsigned i; @@ -785,3153 +677,3 @@ static void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, slots[i].sym->borrow_mut=left[i].mutable || right[i].mutable; } } - -static FeNode *find_const_node(FeCheck *c, const char *name) -{ - FeNode *n; - for (n=c->ast->root ? c->ast->root->children : 0; n; n=n->next) - if (n->kind==FE_N_CONST && n->text && name && strcmp(n->text,name)==0) - return n; - return 0; -} - -static int format_is_slice_u8(FeType *t); - -static const char *builtin_format(FeCheckerState *s, FeNode *fmt) -{ - FeNode *decl; - FeSym *sym; - if (fmt && fmt->kind==FE_N_LITERAL && fmt->text && fmt->text[0]=='"') - return fmt->text; - if (fmt && fmt->kind==FE_N_IDENT) { - sym=find_symbol(s->scope,fmt->text); - decl=sym && sym->decl && sym->decl->kind==FE_N_CONST ? - sym->decl : find_const_node(s->c,fmt->text); - if (decl && decl->b && decl->b->kind==FE_N_LITERAL && - decl->b->text && decl->b->text[0]=='"') { - if (!decl->a || format_is_slice_u8(fe_type_from_ast(&s->c->types,decl->a))) - return decl->b->text; - } - } - return 0; -} - -static int format_is_slice_u8(FeType *t) -{ - return t && t->kind==FE_TYPE_SLICE && t->elem && - t->elem->kind==FE_TYPE_INT && strcmp(t->elem->name,"u8")==0; -} - -static int format_is_writer_type(FeType *t) -{ - return t && t->kind==FE_TYPE_STRUCT && - (strcmp(t->name,"Writer")==0 || strcmp(t->name,"io.Writer")==0); -} - -static int format_arg_ok(FeType *t, int verb) -{ - if (!t) return 0; - if (verb=='x') return fe_type_is_integer(t); - if (verb=='c') return t->kind==FE_TYPE_CHAR; - if (verb=='s') return format_is_slice_u8(t); - if (verb=='b') return t->kind==FE_TYPE_BOOL; - if (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_BOOL || - t->kind==FE_TYPE_CHAR) return 1; - return format_is_slice_u8(t) || - (t->kind==FE_TYPE_ENUM && t->is_error); -} - -static void check_format_call(FeCheckerState *s, FeNode *n) -{ - const char *fmt; - FeNode *fmt_node; - FeNode *arg; - FeNode *x; - FeType *t; - unsigned long i,j; - unsigned count=0; - unsigned argc=0; - unsigned offset=0; - int verb; - int bad=0; - int counted=0; - if (strcmp(n->text,"@fprint")==0) offset=1; - fmt_node=n->children; - if (offset) { - if (!fmt_node) { err(s->c,n->loc,"@fprint requires a writer"); return; } - t=check_expr(s,fmt_node); - if (!format_is_writer_type(t)) - err(s->c,fmt_node->loc,"@fprint requires io.Writer"); - fmt_node=fmt_node->next; - } - if (strcmp(n->text,"@sprint")==0) { - if (!fmt_node) { err(s->c,n->loc,"@sprint requires a buffer"); return; } - t=check_expr(s,fmt_node); - if (!format_is_slice_u8(t) || !t->ref_mut) - err(s->c,fmt_node->loc,"@sprint requires []mut u8 buffer"); - fmt_node=fmt_node->next; - } - fmt=builtin_format(s,fmt_node); - if (!fmt) { err(s->c,n->loc,"format must be a comptime string"); return; } - n->aux_text=(char *)fmt; - arg=fmt_node ? fmt_node->next : 0; - for (x=arg;x;x=x->next) { check_expr(s,x); ++argc; } - i=1; - while (fmt[i] && fmt[i]!='"') { - if (fmt[i]=='\\') { if (fmt[i+1]) ++i; ++i; continue; } - if (fmt[i]=='{' && fmt[i+1]=='{') { i+=2; continue; } - if (fmt[i]=='}' && fmt[i+1]=='}') { i+=2; continue; } - if (fmt[i]=='{') { - j=i+1; - while (fmt[j] && fmt[j]!='}') ++j; - if (!fmt[j]) { err(s->c,n->loc,"unterminated format placeholder"); bad=1; break; } - if (j==i+1) verb=' '; else if (j==i+2) verb=(unsigned char)fmt[i+1]; else verb='?'; - if (verb!=' ' && verb!='x' && verb!='c' && verb!='s' && verb!='b') { - err(s->c,n->loc,"unsupported format verb"); bad=1; - } - if (!arg) { - err(s->c,n->loc,"format argument count mismatch"); - bad=1; counted=1; - } - else { - t=arg->sem_type; - if (verb==' ' && t && t->kind==FE_TYPE_ENUM && t->is_error) verb='s'; - if (!format_arg_ok(t,verb)) { err(s->c,arg->loc,"no fmt writer for argument type"); bad=1; } - arg=arg->next; - } - ++count; i=j+1; continue; - } - if (fmt[i]=='}') { err(s->c,n->loc,"unmatched '}' in format"); bad=1; } - ++i; - } - /* Running out of arguments mid-string already said this. Saying it again - once the whole string has been walked adds nothing. */ - if (count!=argc && !counted) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } - (void)bad; -} - -static int is_format_builtin(const char *name) -{ - return name && (strcmp(name,"@print")==0 || strcmp(name,"@fprint")==0 || - strcmp(name,"@sprint")==0); -} - -static int lvalue_writable(FeCheckerState *s, FeNode *n) -{ - FeSym *sym; - FeType *t; - if (!n) return 0; - if (n->kind == FE_N_IDENT) { - sym=find_symbol(s->scope,n->text ? n->text : ""); - return sym ? sym->mutable : 0; - } - if (n->kind == FE_N_MEMBER) { - t=n->a ? n->a->sem_type : 0; - if (t && t->kind==FE_TYPE_REF && n->b && n->b->text && - strcmp(n->b->text,"^")==0) return t->ref_mut; - return lvalue_writable(s,n->a); - } - if (n->kind == FE_N_INDEX) return lvalue_writable(s,n->a); - return 0; -} - -static int has_field(FeNode *list, const char *name) -{ - FeNode *f; - for (f=list; f; f=f->next) - if (f->text && name && strcmp(f->text,name)==0) return 1; - return 0; -} - -/* A field of a type declared elsewhere is reachable only with `pub`. Inside - the declaring unit every field is reachable, `pub` or not. */ -static int field_is_visible(FeCheckerState *s, const FeType *t, - const FeFieldType *field) -{ - if (!t || !t->unit) return 1; - if (s->c->types.unit_name && - strcmp(t->unit,s->c->types.unit_name)==0) return 1; - return field && field->ast_node && - (field->ast_node->flags & FE_NODE_PUB)!=0; -} - -/* The field list of a struct literal, once the type is known. Reached from - both `Type{...}` and `binding.Type{...}`. */ -static FeType *check_struct_fields(FeCheckerState *s, FeNode *n, FeType *t) -{ - FeFieldType *field; - FeNode *f; - FeType *v; - unsigned i; - for(f=n->children;f;f=f->next) if(f->kind==FE_N_FIELD) { - if(has_field(f->next,f->text)) { err(s->c,f->loc,"duplicate struct field"); } - field=fe_type_field(t,f->text); - if(!field) { err(s->c,f->loc,"invalid struct field"); continue; } - if(!field_is_visible(s,t,field)) { - err(s->c,f->loc,"field is private to its unit"); - continue; - } - v=check_expr(s,f->a); - mark_moved(s,f->a,v); - if(!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"struct field type mismatch"); - } - for(i=0;ifield_count;i++) if(!has_field(n->children,t->fields[i].name)) err(s->c,n->loc,"missing struct field"); - n->sem_type=t; return t; -} - -static FeType *check_struct_init(FeCheckerState *s, FeNode *n) -{ - FeType *t; - FeFieldType *field; - FeNode *f; - FeType *v; - FeType *et; - FeVariantType *variant; - if (n->a && n->a->kind == FE_N_MEMBER) { - FeUnit *home=binding_unit(s,n->a->a); - if (home) { - /* `binding.Type{...}` names a type in another unit. */ - const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; - FeNode *decl=unit_type_decl(s->c,home,want); - t=unit_type(s->c,home,want); - if (!t || !decl) { err(s->c,n->a->loc,"unknown name"); return unknown(s->c); } - if (!decl_is_public(decl)) { - err(s->c,n->a->loc,"type is private to its unit"); - return unknown(s->c); - } - if (t->kind!=FE_TYPE_STRUCT) { - err(s->c,n->loc,"unknown struct type"); - return unknown(s->c); - } - return check_struct_fields(s,n,t); - } - et=check_expr(s,n->a->a); - variant=et && et->kind==FE_TYPE_ENUM ? - fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; - if (!variant) { err(s->c,n->loc,"invalid enum variant"); return unknown(s->c); } - if (variant->field_count != 0) { - for (f=n->children; f; f=f->next) { - if (f->kind != FE_N_FIELD) continue; - field=0; - if (variant->fields) { - unsigned i; - for(i=0;ifield_count;i++) if(strcmp(variant->fields[i].name,f->text)==0) field=&variant->fields[i]; - } - if (!field) { err(s->c,f->loc,"invalid enum payload field"); continue; } - v=check_expr(s,f->a); - mark_moved(s,f->a,v); - if (!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"enum payload type mismatch"); - } - } else if (n->children) err(s->c,n->loc,"empty enum variant cannot have payload"); - n->sem_type=et; return et; - } - t=fe_type_intern(&s->c->types,n->text ? n->text : ""); - if (!t || t->kind!=FE_TYPE_STRUCT) { err(s->c,n->loc,"unknown struct type"); return unknown(s->c); } - return check_struct_fields(s,n,t); -} - -static FeType *check_array_init(FeCheckerState *s, FeNode *n) -{ - FeNode *x; FeType *elem=0; FeType *v; unsigned long count=0; - for(x=n->children;x;x=x->next) { v=check_expr(s,x); mark_moved(s,x,v); if(!elem) elem=v; else if(!compatible(elem,v,x)&&v->kind!=FE_TYPE_UNKNOWN) err(s->c,x->loc,"array element type mismatch"); ++count; } - if(!elem) elem=unknown(s->c); - n->sem_type=fe_type_array(&s->c->types,count,elem); return n->sem_type; -} - -static int array_slice_lvalue(FeNode *n) -{ - return n && (n->kind==FE_N_IDENT || n->kind==FE_N_MEMBER || - n->kind==FE_N_INDEX); -} - -static FeType *check_index(FeCheckerState *s, FeNode *n) -{ - FeType *base=check_expr(s,n->a); FeType *idx; FeType *elem; - if(!fe_type_is_indexable(base)) { err(s->c,n->loc,"indexing requires an array or slice"); return unknown(s->c); } - if(n->b) { idx=check_expr(s,n->b); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"index must be an integer"); } - if(n->c || !n->b) { - if (base->kind==FE_TYPE_ARRAY && !array_slice_lvalue(n->a)) - err(s->c,n->loc,"array slicing requires a stable lvalue"); - if(n->c) { - idx=check_expr(s,n->c); - if(known(idx)&&!fe_type_is_integer(idx)) - err(s->c,n->loc,"slice bound must be an integer"); - } - elem=base->elem; - n->sem_type=(base->kind==FE_TYPE_SLICE ? base->ref_mut : - lvalue_writable(s,n->a)) ? - fe_type_mut_slice(&s->c->types,elem) : - fe_type_slice(&s->c->types,elem); - return n->sem_type; - } - n->sem_type=base->elem; return n->sem_type; -} - -static FeType *check_identifier(FeCheckerState *s, FeNode *n) -{ - FeSym *sym; - sym = find_symbol(s->scope, n->text ? n->text : ""); - if (!sym) { - FeType *named=fe_type_intern(&s->c->types,n->text ? n->text : ""); - if(named->kind==FE_TYPE_STRUCT || named->kind==FE_TYPE_ENUM) { n->sem_type=named; return named; } - if(named->kind!=FE_TYPE_UNKNOWN) { - err(s->c, n->loc, "a type is not a value here"); - return unknown(s->c); - } - err(s->c, n->loc, "unknown name"); - return unknown(s->c); - } - n->cname = sym->cname; - n->sem_type = sym->type; - if (!sym->fn) { - fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); - sym->moved=sym->own.move; - } - return sym->type; -} - -static FeType *check_expr_core(FeCheckerState *s, FeNode *n) -{ - FeCheck *c = s->c; - FeType *a; - FeType *b; - FeSym *sym; - FeNode *x; - FeNode *param; - FeNode *arg; - FeType *et; - FeFieldType *field; - FeVariantType *variant; - const char *op; - if (!n) return unknown(c); - if (n->kind == FE_N_IDENT) - return check_identifier(s, n); - if (n->kind == FE_N_LITERAL) { - if (!n->text) return unknown(c); - if (strcmp(n->text, "true") == 0 || strcmp(n->text, "false") == 0) - a = fe_type_intern(&c->types, "bool"); - else if (n->text[0] == '\'') - a = fe_type_intern(&c->types, "char"); - else if (n->text[0] == '"') - a = fe_type_intern(&c->types, "str"); - else - a = fe_type_intern(&c->types, "i32"); - n->sem_type = a; - return a; - } - if (n->kind == FE_N_STRUCT_INIT) return check_struct_init(s,n); - if (n->kind == FE_N_ARRAY_INIT) return check_array_init(s,n); - if (n->kind == FE_N_INDEX) return check_index(s,n); - if (n->kind == FE_N_MATCH) { check_match(s,n); n->sem_type=unknown(c); return n->sem_type; } - if (n->kind == FE_N_UNARY) { - a = check_expr(s, n->a); - op = n->text ? n->text : ""; - if (strcmp(op, "not") == 0) { - if (known(a) && a->kind != FE_TYPE_BOOL) - err(c, n->loc, "'not' requires bool"); - a = fe_type_intern(&c->types, "bool"); - } else if (strcmp(op, "-") == 0) { - if (known(a) && !fe_type_is_integer(a)) - err(c, n->loc, "unary '-' requires integer"); - } else if (strcmp(op, "try") == 0) { - /* SPEC 6.4: try is only allowed inside a function returning an error - union. Checked on the expression rather than on the statement so - that it also covers `var x = try e;` and `x = try e;`, which the - statement-level check walked straight past. */ - if (!s->ret || s->ret->kind != FE_TYPE_ERROR_UNION) - err(c,n->loc,"try requires an enclosing error result"); - if (a && a->kind==FE_TYPE_ERROR_UNION) - a=a->error_value; - else { - err(c,n->loc,"try requires an error result"); - a=unknown(c); - } - } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { - if (strcmp(op,"&mut")==0 && a && a->kind==FE_TYPE_REF && !a->ref_mut) - err(c,n->loc,"cannot create mutable borrow from a shared reference"); - own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); - a=fe_type_ref(&c->types,a,strcmp(op,"&mut")==0); - } - n->sem_type = a; - return a; - } - if (n->kind == FE_N_TYPE && n->text && strcmp(n->text, "as") == 0) { - a = check_expr(s, n->a); - b = node_type(c, n->b); - if (b->kind == FE_TYPE_VOID) - err(c, n->loc, "cast target cannot be void"); - else if (known(a) && known(b) && !explicit_castable(a,b)) - err(c, n->loc, "'as' requires integer or char types"); - n->sem_type = b; - return b; - } - if (n->kind == FE_N_BINARY) { - a = check_expr(s, n->a); - b = check_expr(s, n->b); - op = n->text ? n->text : ""; - if (strcmp(op, "and") == 0 || strcmp(op, "or") == 0) { - if ((known(a) && a->kind != FE_TYPE_BOOL) || - (known(b) && b->kind != FE_TYPE_BOOL)) - err(c, n->loc, "logical operator requires bool operands"); - a = fe_type_intern(&c->types, "bool"); - } else if (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 || - strcmp(op, "<") == 0 || strcmp(op, "<=") == 0 || - strcmp(op, ">") == 0 || strcmp(op, ">=") == 0) { - if (known(a) && known(b) && !fe_type_equal(a, b) && - !compatible(a, b, n->b) && !compatible(b, a, n->a)) - err(c, n->loc, "comparison operands have different types"); - else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && - ((known(a) && !ordered_type(a)) || - (known(b) && !ordered_type(b)))) - err(c, n->loc, "ordering requires integer or char operands"); - a = fe_type_intern(&c->types, "bool"); - } else { - if ((known(a) && !fe_type_is_integer(a)) || - (known(b) && !fe_type_is_integer(b)) || - (known(a) && known(b) && !fe_type_equal(a, b) && - !compatible(a, b, n->b) && !compatible(b, a, n->a))) - err(c, n->loc, - "arithmetic operands must have the same integer type"); - } - n->sem_type = a; - return a; - } - if (n->kind == FE_N_CALL) { - if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"drop")==0) { - err(c,n->loc,"drop may only be invoked by scope cleanup"); - return unknown(c); - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { - FeNode *arg=n->children; - if (strcmp(n->a->b->text,"destroy")==0) { - a=arg ? check_expr(s,arg) : unknown(c); - if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) - err(c,n->loc,"mem.destroy requires exactly one owned pointer"); - else - mark_moved(s,arg,a); - n->sem_type=fe_type_intern(&c->types,"void"); - return n->sem_type; - } - if (strcmp(n->a->b->text,"create")==0) { - if (!arg || arg->next) - err(c,n->loc,"mem.create requires exactly one value"); - a=arg ? check_expr(s,arg) : unknown(c); - if(arg) mark_moved(s,arg,a); - a=fe_type_owned(&c->types,a); - n->sem_type=fe_type_error_union(&c->types,a); - return n->sem_type; - } - if (strcmp(n->a->b->text,"alloc_slice")==0) { - FeNode *count=arg ? arg->next : 0; - FeType *item; - if(!arg || arg->kind!=FE_N_IDENT || !count || count->next) - err(c,n->loc,"mem.alloc_slice requires a type and length"); - item=arg && arg->kind==FE_N_IDENT ? - fe_type_intern(&c->types,arg->text) : unknown(c); - b=count ? check_expr(s,count) : unknown(c); - if(known(b) && !fe_type_is_integer(b)) - err(c,count->loc,"slice length must be an integer"); - /* Freshly allocated storage is owned outright, so it is - writable: there is nobody else to disturb. */ - a=fe_type_owned(&c->types,fe_type_mut_slice(&c->types,item)); - n->sem_type=fe_type_error_union(&c->types,a); - return n->sem_type; - } - if (strcmp(n->a->b->text,"replace")==0) { - FeNode *value=arg ? arg->next : 0; - if(!arg || !value || value->next) - err(c,n->loc,"mem.replace requires destination and value"); - a=arg ? check_expr(s,arg) : unknown(c); - if(!a || a->kind!=FE_TYPE_REF || !a->ref_mut || - !arg->a || !lvalue_writable(s,arg->a)) - err(c,n->loc,"mem.replace destination must be a mutable place"); - b=value ? check_expr(s,value) : unknown(c); - if(a && a->kind==FE_TYPE_REF && !compatible(a->elem,b,value)) - err(c,value->loc,"mem.replace value type mismatch"); - if(value) mark_moved(s,value,b); - n->sem_type=a && a->kind==FE_TYPE_REF ? a->elem : unknown(c); - fe_type_require_replace(&c->types,n->sem_type); - return n->sem_type; - } - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && - strcmp(n->a->b->text,"null_writer")==0) { - FeNode *arg=n->children; - if (arg) err(c,n->loc,"io.null_writer takes no arguments"); - n->sem_type=fe_type_intern(&c->types,"io.Writer"); - return n->sem_type; - } - if (n->text && is_format_builtin(n->text)) { - check_format_call(s,n); - if (strcmp(n->text,"@print")==0) - n->sem_type=fe_type_intern(&c->types,"void"); - else if (strcmp(n->text,"@sprint")==0) - n->sem_type=fe_type_intern(&c->types,"usize"); - else - n->sem_type=fe_type_error_union(&c->types,fe_type_intern(&c->types,"void")); - return n->sem_type; - } - if (!n->a && n->text && (strcmp(n->text,"@size_of")==0 || strcmp(n->text,"@align_of")==0)) { - FeNode *type_arg=n->children; - FeType *target=type_arg && type_arg->kind==FE_N_IDENT ? fe_type_intern(&c->types,type_arg->text) : unknown(c); - if(!target || !known(target)) err(c,n->loc,"size/align requires a known type"); - n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; - } - if (n->a && n->a->kind == FE_N_MEMBER) { - FeNode *method; - FeNode *self_param; - FeUnit *home=binding_unit(s,n->a->a); - if (home) { - const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; - FeSym *fsym=unit_member(c,home,want); - if (!fsym) { - err(c,n->a->loc,"unknown name"); - for (x=n->children;x;x=x->next) check_expr(s,x); - return unknown(c); - } - if (!decl_is_public(fsym->decl)) { - err(c,n->a->loc,"name is private to its unit"); - for (x=n->children;x;x=x->next) check_expr(s,x); - return unknown(c); - } - if (decl_is_generic(fsym->fn)) - return check_generic_call(s,n,fsym,home); - return check_call_args(s,n,fsym,home->name,0); - } - { - int names_type=0; - FeType *owner_type=type_from_expr(s,n->a->a,&names_type); - if (names_type && owner_type && - owner_type->kind==FE_TYPE_STRUCT) { - FeNode *m=type_method(owner_type, - n->a->b ? n->a->b->text : ""); - if (!m) { err(c,n->a->loc,"unknown method"); return unknown(c); } - if (!method_is_static(m)) { - err(c,n->loc,"method requires a receiver"); - return unknown(c); - } - return check_static_method_call(s,n,owner_type,m); - } - } - et=check_expr(s,n->a->a); - /* A method can be reached through a reference or an owner as well - as through the value itself. */ - if (et && (et->kind==FE_TYPE_REF || et->kind==FE_TYPE_OWNED) && - et->elem && et->elem->kind==FE_TYPE_STRUCT && - find_method(c,et->elem,n->a->b ? n->a->b->text : "")) - et=et->elem; - method=et && et->kind==FE_TYPE_STRUCT ? - find_method(c,et,n->a->b ? n->a->b->text : "") : 0; - if(method) { - FeBindSave msave; - int bound=0; - self_param=method->a ? method->a->children : 0; - if(!self_param) { - err(c,n->loc,"method requires self parameter"); - return unknown(c); - } - /* A method of a generic instance reads its signature with that - instance's arguments bound. */ - if (et->bind_count) { - push_instance_bindings(c,&msave,et); - bind_self(c,et); - bound=1; - } - a=method_type(c,self_param->a,et); - if(a->kind==FE_TYPE_REF && a->ref_mut && - !lvalue_writable(s,n->a->a)) - err(c,n->loc,"mutable method requires a mutable receiver"); - if(a->kind!=FE_TYPE_REF) mark_moved(s,n->a->a,et); - param=self_param->next; - arg=n->children; - while(param && arg) { - a=check_expr(s,arg); - b=method_type(c,param->a,et); - if(!compatible(b,a,arg) && a->kind!=FE_TYPE_UNKNOWN) - err(c,arg->loc,"method argument type mismatch"); - mark_moved(s,arg,a); - param=param->next; - arg=arg->next; - } - if(param || arg) err(c,n->loc,"wrong number of method arguments"); - n->sem_decl=method; - n->sem_type=method->b ? method_type(c,method->b,et) : - fe_type_intern(&c->types,"void"); - if (bound) { - pop_bindings(c,&msave); - check_instance_method(s,et,method,n->loc,n); - } - return n->sem_type; - } - if (et && (et->kind==FE_TYPE_SLICE || et->kind==FE_TYPE_STR) && - n->a->b && n->a->b->text && - strcmp(n->a->b->text,"trim")==0) { - if (n->children) err(c,n->loc,"trim takes no arguments"); - n->sem_type=fe_type_slice(&c->types,et->elem); - return n->sem_type; - } - variant=et && et->kind==FE_TYPE_ENUM ? - fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; - arg=n->children; - if (!variant) { err(c,n->loc,"invalid enum variant constructor"); return unknown(c); } - if (variant->field_count==1 && arg) { - FeType *av=check_expr(s,arg); - if(!compatible(variant->fields[0].type,av,arg)&&av->kind!=FE_TYPE_UNKNOWN) err(c,arg->loc,"enum payload type mismatch"); - } else if (variant->field_count != 0 || arg) err(c,n->loc,"wrong enum payload arity"); - n->sem_type=et; return et; - } - if (n->a && n->a->kind == FE_N_IDENT) { - sym = find_symbol(s->scope, n->a->text ? n->a->text : ""); - if (!sym) { - err(c, n->loc, "unknown function"); - return unknown(c); - } - if (decl_is_generic(sym->fn)) - return check_generic_call(s,n,sym,current_unit(c)); - return check_call_args(s, n, sym, 0, 0); - } - for (x = n->children; x; x = x->next) check_expr(s, x); - return unknown(c); - } - if (n->kind == FE_N_MEMBER) { - if (is_error_set_member(s,n)) { - n->sem_type=fe_type_intern(&c->types,"core.Error"); - return n->sem_type; - } - if (n->a && n->a->kind==FE_N_IDENT && n->a->text && - strcmp(n->a->text,"io")==0 && n->b && n->b->text && - (strcmp(n->b->text,"stdout")==0 || - strcmp(n->b->text,"stderr")==0)) { - n->sem_type=fe_type_intern(&c->types,"io.Writer"); - return n->sem_type; - } - a=check_expr(s,n->a); - if (a->kind == FE_TYPE_REF && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - n->sem_type=a->elem; - return a->elem; - } - if(a->kind==FE_TYPE_REF && a->elem && - a->elem->kind==FE_TYPE_STRUCT) { - field=fe_type_field(a->elem,n->b ? n->b->text : ""); - if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } - n->sem_type=field->type; - return field->type; - } - if (a->kind == FE_TYPE_OWNED && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - n->sem_type=a->elem; - return a->elem; - } - if(a->kind==FE_TYPE_STRUCT) { - field=fe_type_field(a,n->b ? n->b->text : ""); - if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } - n->sem_type=field->type; return field->type; - } - if(a->kind==FE_TYPE_ENUM) { - if(!fe_type_variant(a,n->b ? n->b->text : "")) err(c,n->loc,"unknown enum variant"); - n->sem_type=a; return a; - } - if((a->kind==FE_TYPE_SLICE || a->kind==FE_TYPE_STR) && n->b && - strcmp(n->b->text,"n")==0) { - n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; - } - return unknown(c); - } - return unknown(c); -} - -/* `base_in` is the already-checked type of a member expression's base. The M7 - lvalue path looks at that base before delegating here, and checking it a - second time reports any ownership violation on it a second time too. */ -static FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, - FeType *base_in) -{ - FeSym *sym; - FeType *base; - FeFieldType *field; - if (n && n->kind == FE_N_IDENT) { - sym = find_symbol(s->scope, n->text ? n->text : ""); - if (!sym) { - err(s->c, n->loc, "unknown name"); - return unknown(s->c); - } - if (sym->fn) { - err(s->c, n->loc, "function is not assignable"); - return unknown(s->c); - } - if (!sym->mutable) - err(s->c, n->loc, "cannot assign to immutable let"); - n->cname = sym->cname; - n->sem_type = sym->type; - if (read) { - fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); - sym->moved=sym->own.move; - } - return sym->type; - } - if (n && n->kind == FE_N_MEMBER) { - base=base_in ? base_in : check_expr(s,n->a); - if (base && base->kind == FE_TYPE_REF && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - if (!base->ref_mut) - err(s->c,n->loc,"cannot write through shared reference"); - n->sem_type=base->elem; - return base->elem; - } - if(base && base->kind==FE_TYPE_REF && base->elem && - base->elem->kind==FE_TYPE_STRUCT) { - if(!base->ref_mut) - err(s->c,n->loc,"cannot write through shared reference"); - field=fe_type_field(base->elem,n->b ? n->b->text : ""); - if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } - n->sem_type=field->type; - return field->type; - } - if (base && base->kind == FE_TYPE_OWNED && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - n->sem_type=base->elem; - return base->elem; - } - if (!lvalue_writable(s,n->a)) - err(s->c,n->loc,"cannot assign through immutable value"); - field=base && base->kind==FE_TYPE_STRUCT ? fe_type_field(base,n->b ? n->b->text : "") : 0; - if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } - n->sem_type=field->type; return field->type; - } - if (n && n->kind == FE_N_INDEX) { - base=check_index(s,n); - if (n->a && n->a->sem_type && - n->a->sem_type->kind == FE_TYPE_SLICE && - !n->a->sem_type->ref_mut) - err(s->c,n->loc,"cannot write through shared slice"); - else if (n->a && n->a->sem_type && - n->a->sem_type->kind != FE_TYPE_SLICE && - !lvalue_writable(s,n->a)) - err(s->c,n->loc,"cannot assign through immutable value"); - return base; - } - if (n) err(s->c, n->loc, "assignment requires a variable"); - return unknown(s->c); -} - -static int compound_operator(const char *op) -{ - return op && strcmp(op, "=") != 0; -} - -static void check_match(FeCheckerState *s, FeNode *n) -{ - FeType *value; - FeNode *arm; - FeVariantType *variant; - int seen[256]; - int wildcard=0; - FeFlowSlot base[64], merged[64], current[64]; - unsigned flow_count; - int have_merged=0; - unsigned i; - for(i=0;i<256U;i++) seen[i]=0; - value=check_expr(s,n->a); - if(!value || value->kind!=FE_TYPE_ENUM) { err(s->c,n->loc,"match requires an enum value"); return; } - flow_count=flow_capture(s->scope,base,64); - for(arm=n->children;arm;arm=arm->next) { - FeScope *old=s->scope; - flow_restore(base,flow_count); - if(arm->text && strcmp(arm->text,"_")==0) wildcard=1; - else { - variant=fe_type_variant(value,arm->text); - if(!variant) { err(s->c,arm->loc,"unknown match variant"); continue; } - if(variant->tag<256U) { - if(seen[variant->tag]) err(s->c,arm->loc,"duplicate match variant"); - seen[variant->tag]=1; - } - s->scope=scope_new(s,old); - if(variant->field_count==1 && arm->children) { - add_symbol(s,s->scope,arm->children->text,variant->fields[0].type,0,0,1, - local_cname(s->c,arm->children->text),arm->children); - } else if(variant->field_count>0) { - FeNode *b=arm->children; - for(i=0;ifield_count && b;i++,b=b->next) { - FeFieldType *f=&variant->fields[i]; - add_symbol(s,s->scope,b->text,f->type,0,0,1, - local_cname(s->c,b->text),b); - } - } - } - if(arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); - else if(arm->a) check_expr(s,arm->a); - s->scope=old; - flow_capture(s->scope,current,flow_count); - if(!have_merged) { - for(i=0;ivariant_count && i<256U;i++) if(!seen[i]) err(s->c,n->loc,"non-exhaustive match"); -} - -static void check_for(FeCheckerState *s, FeNode *n) -{ - FeType *start; - FeType *finish; - FeType *elem; - FeType *ref_type; - FeSym *iter_sym; - char *index_cname; - char *item_cname; - int iter_mut; - FeScope *old=s->scope; - if(!n->c) { - start=check_expr(s,n->a); - if (!fe_type_is_indexable(start)) { - err(s->c,n->loc,"for iterable must be an array, slice, or str"); - return; - } - elem=start->elem; - iter_sym=0; - if (n->a && n->a->kind==FE_N_IDENT) - iter_sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); - else if (n->a && n->a->kind==FE_N_INDEX && n->a->a && - n->a->a->kind==FE_N_IDENT) - iter_sym=find_symbol(s->scope,n->a->a->text ? n->a->a->text : ""); - iter_mut=start->kind==FE_TYPE_SLICE ? start->ref_mut : - (iter_sym && iter_sym->mutable); - ref_type=fe_type_ref(&s->c->types,elem,iter_mut); - if (iter_mut) n->flags |= 4U; - s->scope=scope_new(s,old); - if (n->aux_text) { - index_cname=local_cname(s->c,n->text ? n->text : "index"); - item_cname=local_cname(s->c,n->aux_text); - add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, - index_cname,n); - add_symbol(s,s->scope,n->aux_text,ref_type,0,iter_mut,1, - item_cname,0); - n->cname=index_cname; - n->aux_cname=item_cname; - } else { - item_cname=local_cname(s->c,n->text ? n->text : "item"); - add_symbol(s,s->scope,n->text,ref_type,0,iter_mut,1, - item_cname,n); - n->cname=item_cname; - } - check_stmt(s,n->b); - s->scope=old; - return; - } - start=check_expr(s,n->a); - finish=check_expr(s,n->c); - if(known(start)&&!fe_type_is_integer(start)) err(s->c,n->loc,"range start must be integer"); - if(known(finish)&&!fe_type_is_integer(finish)) err(s->c,n->loc,"range end must be integer"); - s->scope=scope_new(s,old); - index_cname=local_cname(s->c,n->text ? n->text : "index"); - add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, - index_cname,n); - n->cname=index_cname; - check_stmt(s,n->b); - s->scope=old; -} - -static void check_type_cycle(FeCheck *c, FeType *t) -{ - unsigned i; - FeType *next; - if (!t || t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR || - t->kind == FE_TYPE_REF || t->kind == FE_TYPE_OWNED || - t->kind == FE_TYPE_INT || t->kind == FE_TYPE_BOOL || - t->kind == FE_TYPE_CHAR || t->kind == FE_TYPE_VOID || - t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) return; - if (t->kind == FE_TYPE_ERROR_UNION) { - check_type_cycle(c,t->error_value); - return; - } - if (t->cycle_state == 1) { - if (c->ast->root) err(c, c->ast->root->loc, "by-value recursive type"); - return; - } - if (t->cycle_state == 2) return; - t->cycle_state = 1; - if (t->kind == FE_TYPE_ARRAY) { - check_type_cycle(c,t->elem); - } else if (t->kind == FE_TYPE_STRUCT) { - for (i=0;ifield_count;i++) { - if (!t->fields[i].type && t->fields[i].ast_node) - t->fields[i].type=fe_type_from_ast(&c->types,t->fields[i].ast_node->a); - check_type_cycle(c,t->fields[i].type); - } - } else if (t->kind == FE_TYPE_ENUM) { - for (i=0;ivariant_count;i++) { - unsigned j; - for (j=0;jvariants[i].field_count;j++) { - if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) - t->variants[i].fields[j].type=fe_type_from_ast(&c->types, - t->variants[i].fields[j].ast_node->a); - next=t->variants[i].fields[j].type; - check_type_cycle(c,next); - } - } - } - t->cycle_state=2; -} - -static void check_type_cycles(FeCheck *c) -{ - FeType *t; - for (t=c->types.types;t;t=t->next) t->cycle_state=0; - for (t=c->types.types;t;t=t->next) check_type_cycle(c,t); -} - -static int own_ast_reference_type(FeNode *type) -{ - if (!type || !type->text) return 0; - return strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || - (strcmp(type->text,"[")==0 && !type->a) || strcmp(type->text,"str")==0; -} - -static int own_ast_pointer_to_reference(FeNode *type) -{ - return type && type->text && strcmp(type->text,"*")==0 && - own_ast_reference_type(type->a); -} - -static void check_reference_storage(FeCheck *c, FeNode *decl) -{ - FeNode *m; - if (!decl) return; - if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { - for (m=decl->children;m;m=m->next) - if (m->kind==FE_N_FIELD && - (own_ast_reference_type(m->a) || own_ast_pointer_to_reference(m->a))) - err(c,m->loc,"reference type is not allowed in aggregate storage"); - } - if ((decl->kind==FE_N_GLOBAL || decl->kind==FE_N_CONST) && decl->a && - own_ast_reference_type(decl->a) && - !(decl->kind==FE_N_CONST && decl->a->text && strcmp(decl->a->text,"str")==0)) - err(c,decl->loc,"reference type is not allowed in global storage"); - if (decl->kind==FE_N_FN && decl->b && own_ast_pointer_to_reference(decl->b)) - err(c,decl->b->loc,"reference type is not allowed as a pointer target"); - if (decl->kind==FE_N_FN) - for (m=decl->a ? decl->a->children : 0;m;m=m->next) - if (own_ast_pointer_to_reference(m->a)) - err(c,m->loc,"reference type is not allowed as a pointer target"); -} - -static int own_return_from_allowed_root(FeCheckerState *s, FeNode *expr) -{ - FeSym *root; - FeNode *p; - unsigned refs=0; - if (!expr) return 0; - root=own_root_symbol(s,expr); - if (!root) return 1; /* Static-producing builtins/methods are checked by - their declared R8 interface. */ - if (own_is_global(s,root)) - return root->decl && root->decl->kind==FE_N_GLOBAL && - (root->decl->flags & 2U); - if (!root->decl || root->decl->kind!=FE_N_PARAM) return 0; - for (p=s->fn_node && s->fn_node->a ? s->fn_node->a->children : 0; - p;p=p->next) { - FeType *t=p->sem_type ? p->sem_type : node_type(s->c,p->a); - if (fe_own_is_reference_like(t)) ++refs; - } - if (s->fn_node && s->fn_node->text && refs && - root->name && strcmp(root->name,"self")==0) return 1; - return refs==1; -} - -static void check_stmt_core(FeCheckerState *s, FeNode *n) -{ - FeCheck *c = s->c; - FeScope *old; - FeType *a; - FeType *b; - FeSym *sym; - FeNode *x; - int initialized; - if (!n) return; - switch (n->kind) { - case FE_N_BLOCK: - old = s->scope; - s->scope = scope_new(s, old); - for (x = n->children; x; x = x->next) { - check_stmt(s,x); - own_release_after_stmt(s,s->scope,x,0); - } - own_release_after_stmt(s,s->scope,n,1); - s->scope = old; - break; - case FE_N_LET: - case FE_N_CONST: - a = n->a ? node_type(c, n->a) : unknown(c); - b = check_expr(s, n->b); - if (!n->a) a = b; - if (a->kind == FE_TYPE_VOID) - err(c, n->loc, "variable cannot have void type"); - if (n->a && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) - err(c, n->loc, "initializer type mismatch"); - if (b->kind == FE_TYPE_VOID) - err(c, n->loc, "void expression cannot initialize a variable"); - if (n->kind==FE_N_LET && a->kind==FE_TYPE_SLICE && a->ref_mut) - err(c,n->loc,"let cannot bind a mutable slice"); - mark_moved(s,n->b,b); - sym=add_symbol(s, s->scope, n->text, a, 0, 0, 1, - local_cname(c, n->text ? n->text : "local"), n); - if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && - (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { - sym->borrow_root=own_root_symbol(s,n->b->a); - sym->borrow_mut=strcmp(n->b->text,"&mut")==0; - sym->borrow_defer=s->defer_depth != 0 || - own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); - } - own_bind_derived_call(s,sym,n->b); - break; - case FE_N_VAR: - a = n->a ? node_type(c, n->a) : unknown(c); - if (!n->b && !n->a) - err(c, n->loc, "uninitialized var requires an explicit type"); - b = n->b ? check_expr(s, n->b) : unknown(c); - if (!n->a && n->b) a = b; - if (a->kind == FE_TYPE_VOID) - err(c, n->loc, "variable cannot have void type"); - if (n->b && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) - err(c, n->loc, "initializer type mismatch"); - if (b->kind == FE_TYPE_VOID) - err(c, n->loc, "void expression cannot initialize a variable"); - mark_moved(s,n->b,b); - initialized = n->b != 0; - sym=add_symbol(s, s->scope, n->text, a, 0, 1, initialized, - local_cname(c, n->text ? n->text : "local"), n); - if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && - (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { - sym->borrow_root=own_root_symbol(s,n->b->a); - sym->borrow_mut=strcmp(n->b->text,"&mut")==0; - sym->borrow_defer=s->defer_depth != 0 || - own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); - } - own_bind_derived_call(s,sym,n->b); - break; - case FE_N_ASSIGN: - b = check_expr(s, n->b); - a = check_lvalue(s, n->a, compound_operator(n->text)); - if (!compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) - err(c, n->loc, "assignment type mismatch"); - mark_moved(s,n->b,b); - sym = n->a && n->a->kind == FE_N_IDENT ? - find_symbol(s->scope, n->a->text) : 0; - if (sym && sym->mutable) { - sym->initialized = 1; - fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); - sym->moved=sym->own.move; - if (n->b && n->b->kind==FE_N_UNARY && n->b->text && - (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && - fe_own_is_reference_like(sym->type)) { - FeSym *root=own_root_symbol(s,n->b->a); - if (root && root->owner!=sym->owner) - err(c,n->b->loc,"reference would outlive its source scope"); - else if (root) { - if (sym->borrow_root) { - if (sym->borrow_mut) fe_own_release_exclusive(&sym->borrow_root->own); - else fe_own_release_shared(&sym->borrow_root->own); - } - sym->borrow_root=root; - sym->borrow_mut=strcmp(n->b->text,"&mut")==0; - } - } - } - break; - case FE_N_EXPR_STMT: - /* The enclosing-error-result check lives on the try expression itself, - so a bare `try e;` needs nothing extra here. */ - check_expr(s, n->a); - break; - case FE_N_DEFER: - ++s->defer_depth; - check_stmt(s,n->a); - --s->defer_depth; - break; - case FE_N_IF: { - FeFlowSlot base[64], left[64], right[64]; - FeOwnState *own_base, *own_left, *own_right; - FeFlowBorrow *borrow_base, *borrow_left, *borrow_right; - unsigned flow_count; - a = check_expr(s, n->a); - if (known(a) && a->kind != FE_TYPE_BOOL) - err(c, n->loc, "if condition must be bool"); - flow_count=flow_capture(s->scope,base,64); - own_base=flow_own_new(s,flow_count); - own_left=flow_own_new(s,flow_count); - own_right=flow_own_new(s,flow_count); - borrow_base=flow_borrow_new(s,flow_count); - borrow_left=flow_borrow_new(s,flow_count); - borrow_right=flow_borrow_new(s,flow_count); - flow_own_capture(base,own_base,flow_count); - flow_borrow_capture(base,borrow_base,flow_count); - check_stmt(s, n->b); - flow_capture(s->scope,left,flow_count); - flow_own_capture(left,own_left,flow_count); - flow_borrow_capture(left,borrow_left,flow_count); - flow_restore(base,flow_count); - flow_own_restore(base,own_base,flow_count); - flow_borrow_restore(base,borrow_base,flow_count); - if (n->c) check_stmt(s, n->c); - if (n->c) { - flow_capture(s->scope,right,flow_count); - flow_own_capture(right,own_right,flow_count); - flow_borrow_capture(right,borrow_right,flow_count); - } - else { - unsigned i; - for (i=0;ia); - if (known(a) && a->kind != FE_TYPE_BOOL) - err(c, n->loc, "while condition must be bool"); - flow_count=flow_capture(s->scope,base,64); - own_base=flow_own_new(s,flow_count); - own_body=flow_own_new(s,flow_count); - own_entry2=flow_own_new(s,flow_count); - borrow_base=flow_borrow_new(s,flow_count); - borrow_body=flow_borrow_new(s,flow_count); - borrow_entry2=flow_borrow_new(s,flow_count); - flow_own_capture(base,own_base,flow_count); - flow_borrow_capture(base,borrow_base,flow_count); - if (s->loop_depth < 255U) ++s->loop_depth; - check_stmt(s, n->b); - if (s->loop_depth) --s->loop_depth; - flow_capture(s->scope,body,flow_count); - flow_own_capture(body,own_body,flow_count); - flow_borrow_capture(body,borrow_body,flow_count); - for (i=0;iloop_depth < 255U) ++s->loop_depth; - check_stmt(s,n->b); - if (s->loop_depth) --s->loop_depth; - flow_capture(s->scope,body,flow_count); - flow_own_capture(body,own_body,flow_count); - flow_borrow_capture(body,borrow_body,flow_count); - for(i=0;iloop_depth < 255U) ++s->loop_depth; - check_for(s,n); - if (s->loop_depth) --s->loop_depth; - break; - case FE_N_MATCH: - check_match(s,n); - break; - case FE_N_BREAK: - case FE_N_CONTINUE: - if (!s->loop_depth) err(c,n->loc,"break or continue outside loop"); - break; - case FE_N_RETURN: - b = n->a ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); - if (s->ret && fe_own_is_reference_like(s->ret) && - !own_return_from_allowed_root(s,n->a)) - err(c,n->loc,"reference return must be derived from a parameter or static"); - mark_moved(s,n->a,b); - if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) - err(c, n->loc, "void expression returned from value function"); - else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && - b->kind != FE_TYPE_UNKNOWN && - !compatible(s->ret,b,n->a)) - err(c, n->loc, "return type mismatch"); - break; - case FE_N_UNSAFE: - check_stmt(s, n->a); - break; - default: - break; - } -} - -static void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) -{ - FeCheckerState s; - FeScope *old; - FeNode *x; - FeType *t; - s.c = c; - s.globals = globals; - s.scope = scope_new(&s, globals); - s.ret = fn->b ? node_type(c, fn->b) : fe_type_intern(&c->types, "void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=fn; - fe_own_liveness_init(&s.liveness,&c->arena); - fe_own_collect_last_uses(&s.liveness,fn); - fn->sem_type = s.ret; - for (x = fn->a ? fn->a->children : 0; x; x = x->next) { - t = node_type(c, x->a); - if (t->kind == FE_TYPE_VOID) - err(c, x->loc, "parameter cannot have void type"); - add_symbol(&s, s.scope, x->text, t, 0, 1, 1, - local_cname(c, x->text ? x->text : "arg"), x); - } - old = s.scope; - if (fn->c) check_stmt(&s, fn->c); - s.scope = old; -} - -static void check_method(FeCheck *c, FeNode *fn, FeScope *globals, - FeType *owner) -{ - FeCheckerState s; - FeNode *x; - FeType *t; - s.c=c; - s.globals=globals; - s.scope=scope_new(&s,globals); - s.ret=fn->b ? method_type(c,fn->b,owner) : fe_type_intern(&c->types,"void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=fn; - fe_own_liveness_init(&s.liveness,&c->arena); - fe_own_collect_last_uses(&s.liveness,fn); - fn->sem_type=s.ret; - for(x=fn->a ? fn->a->children : 0; x; x=x->next) { - t=method_type(c,x->a,owner); - x->sem_type=t; - add_symbol(&s,s.scope,x->text,t,0,1,1, - local_cname(c,x->text ? x->text : "arg"),x); - } - if(fn->c) check_stmt(&s,fn->c); -} - -static int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) -{ - if (fe_type_equal(want,got)) return 1; - return compatible(want,got,value); -} - -static FeType *m7_check_expected(FeCheckerState *s, FeNode *value, - FeType *expected) -{ - FeType *actual; - FeM7ContextKind context; - if (!value) return unknown(s->c); - if (fe_m7_is_null(value)) { - if (!fe_m7_can_contextual_null(expected)) { - err(s->c,value->loc,"null requires a contextual optional type"); - value->sem_type=unknown(s->c); - return value->sem_type; - } - value->sem_type=expected; - value->sem_context=expected; - return expected; - } - actual=check_expr(s,value); - if (!expected) return actual; - if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && - m7_actual_compatible(expected->elem,actual,value)) { - value->sem_context=expected; - return expected; - } - if (expected->kind==FE_TYPE_ERROR_UNION) { - context=fe_m7_error_context(&s->c->types,expected,actual); - if (context!=FE_M7_CONTEXT_NONE) { - value->sem_context=expected; - return expected; - } - } - return actual; -} - -static FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) -{ - FeFieldType *field; - FeType *owner; - if (!base) return unknown(s->c); - if (n->text && strcmp(n->text,".?")==0) { - if (base->kind!=FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"optional projection '.?' requires an optional value"); - return unknown(s->c); - } - n->sem_type=base->elem; - return n->sem_type; - } - if (base->kind==FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"optional value must be projected with '.?' first"); - return unknown(s->c); - } - if (base->kind==FE_TYPE_REF && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - n->sem_type=base->elem; - return n->sem_type; - } - if (base->kind==FE_TYPE_OWNED && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - n->sem_type=base->elem; - return n->sem_type; - } - owner=base; - if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && - base->elem && base->elem->kind==FE_TYPE_STRUCT) - owner=base->elem; - if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { - field=fe_type_field(owner,n->b->text); - if (!field) { - err(s->c,n->loc,"unknown struct field"); - return unknown(s->c); - } - n->sem_type=field->type; - return field->type; - } - if (base->kind==FE_TYPE_ENUM && n->b && n->b->text) { - if (!fe_type_variant(base,n->b->text)) - err(s->c,n->loc,"unknown enum variant"); - n->sem_type=base; - return base; - } - if ((base->kind==FE_TYPE_SLICE || base->kind==FE_TYPE_STR) && - n->b && n->b->text && strcmp(n->b->text,"n")==0) { - n->sem_type=fe_type_intern(&s->c->types,"usize"); - return n->sem_type; - } - n->sem_type=unknown(s->c); - return n->sem_type; -} - -static int m7_place_is_projection(FeNode *n) -{ - return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); -} - -/* ------------------------------------------------------------------------- * - * Generics (SPEC 9) - * - * A generic declaration is checked once per distinct list of type arguments. - * Those arguments are bound as types for the length of that check, so a name - * that is a type parameter simply is its argument -- in the body, in field - * types and in the signature alike. An instance is identified by its declaring - * unit, its declaration and the spelling of its arguments, so asking twice - * asks for the same instance, and a chain of new ones is bounded. - * ------------------------------------------------------------------------- */ - -#define FE_GENERIC_DEPTH_MAX 32 - -static unsigned decl_type_param_count(const FeNode *decl) -{ - FeNode *p; - unsigned n=0; - if (!decl) return 0; - if (decl->kind==FE_N_FN) { - for (p=decl->a?decl->a->children:0;p;p=p->next) - if (p->flags & FE_NODE_COMPTIME) ++n; - return n; - } - if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) - for (p=decl->a?decl->a->children:0;p;p=p->next) ++n; - return n; -} - -static FeNode *decl_type_param(const FeNode *decl, unsigned i) -{ - FeNode *p; - unsigned n=0; - if (!decl) return 0; - if (decl->kind==FE_N_FN) { - for (p=decl->a?decl->a->children:0;p;p=p->next) - if (p->flags & FE_NODE_COMPTIME) { if (n==i) return p; ++n; } - return 0; - } - for (p=decl->a?decl->a->children:0;p;p=p->next) { if (n==i) return p; ++n; } - return 0; -} - -static int decl_is_generic(const FeNode *decl) -{ - return decl_type_param_count(decl)!=0; -} - -/* SPEC 9: v0.1 has comptime type parameters and no other kind. */ -static void check_generic_params(FeCheck *c, FeNode *decl) -{ - FeNode *p; - if (!decl || decl->kind!=FE_N_FN) return; - for (p=decl->a?decl->a->children:0;p;p=p->next) { - if (!(p->flags & FE_NODE_COMPTIME)) continue; - if (!p->a || p->a->kind!=FE_N_TYPE || !p->a->text || - strcmp(p->a->text,"type")!=0) - err(c,p->loc,"a comptime parameter must be a type parameter"); - } -} - -static void push_bindings(FeCheck *c, FeBindSave *save, FeNode *decl, - FeType **args, unsigned count) -{ - unsigned i; - save->count=c->types.param_count; - for (i=0;iparams[i]=c->types.params[i]; - c->types.param_count=0; - for (i=0;itypes.params[i].name=p && p->text ? p->text : "?"; - c->types.params[i].type=args[i]; - ++c->types.param_count; - } -} - -/* Restore the bindings recorded on an instance, so a method sees exactly the - environment its type was built with. */ -static void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t) -{ - unsigned i; - save->count=c->types.param_count; - for (i=0;iparams[i]=c->types.params[i]; - c->types.param_count=0; - for (i=0;ibind_count && itypes.params[c->types.param_count++]=t->binds[i]; -} - -static void bind_self(FeCheck *c, FeType *owner) -{ - if (c->types.param_count>=FE_TYPE_PARAM_MAX) return; - c->types.params[c->types.param_count].name="Self"; - c->types.params[c->types.param_count].type=owner; - ++c->types.param_count; -} - -static void pop_bindings(FeCheck *c, const FeBindSave *save) -{ - unsigned i; - for (i=0;itypes.params[i]=save->params[i]; - c->types.param_count=save->count; -} - -/* `unit.Name(arg,arg)` -- the canonical identity of one instance. - Nesting makes the readable spelling grow without bound, and a spelling that - got cut off would make two different instances look like the same one, so - past a length the arguments are written as serial numbers instead. Those are - unique, so identity stays exact even where the spelling stops being - readable. */ -#define FE_GENERIC_NAME_READABLE 200 - -static void instance_key(char *out, const char *unit, const char *name, - FeType **args, unsigned count) -{ - unsigned i; - unsigned long n=0; - unsigned long cap=(unsigned long)FE_GENERIC_NAME_READABLE; - const char *p; - char number[24]; - int readable=1; - for (p=unit?unit:"";*p;++p) { if (nname[0] ? args[i]->name : "?";*p;++p) { - if (nserial : 0U); - for (p=number;*p && ninstance_count;++i) - if (!strcmp(c->instances[i].key,key)) return c->instances[i].cname; - return 0; -} - -static int instance_known(FeCheck *c, const char *key) -{ - unsigned i; - for (i=0;iinstance_count;++i) - if (strcmp(c->instances[i].key,key)==0) return 1; - return 0; -} - -static int instance_record(FeCheck *c, const char *key, FeLoc loc, - FeNode *decl, FeUnit *home, FeType *owner) -{ - FeInstance *inst; - unsigned i; - if (instance_known(c,key)) return 0; - if (c->instance_count>=FE_GENERIC_INSTANCE_MAX) { - err(c,loc,"too many generic instances"); - return -1; - } - inst=&c->instances[c->instance_count]; - strcpy(inst->key,key); - inst->decl=decl; - inst->home=home ? home->name : 0; - inst->owner=owner; - inst->cname=unit_cname(c,key); - /* The bindings in force right now are the ones this instance was built - with, and lowering has to see exactly those again. */ - inst->bind_count=c->types.param_count; - for (i=0;itypes.param_count && ibinds[i]=c->types.params[i]; - ++c->instance_count; - return 1; -} - -/* One step further down a chain of instantiations. Chains that keep producing - new instances are the ones that never end, so the limit counts nesting. */ -static int instance_descend(FeCheck *c, FeLoc loc) -{ - if (c->instance_depth>=FE_GENERIC_DEPTH_MAX) { - err(c,loc,"generic instantiation depth exceeded"); - return 0; - } - ++c->instance_depth; - return 1; -} - -static FeUnit *current_unit(FeCheck *c) -{ - unsigned u; - for (u=0;ubuild->count;++u) - if (strcmp(c->build->units[u].name,c->types.unit_name)==0) - return &c->build->units[u]; - return c->unit; -} - -/* Build `Box(i32)`: the declaration's fields with the parameters bound, under - a name that records which arguments made it. */ -static FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, - const char *key, FeType **args, - unsigned count) -{ - FeBindSave save; - FeType *t; - FeNode *f; - unsigned fields=0; - unsigned i=0; - t=fe_type_intern_unit(&c->types,home->name,key); - if (!t || t->kind!=FE_TYPE_UNKNOWN) return t; - t->kind=FE_TYPE_STRUCT; - t->packed=(decl->flags & FE_NODE_PACKED)!=0; - t->decl_node=decl; - t->bind_count=0; - for (i=0;ibinds[t->bind_count].name=p && p->text ? p->text : "?"; - t->binds[t->bind_count].type=args[i]; - ++t->bind_count; - } - t->cname=unit_cname(c,key); - for (f=decl->children;f;f=f->next) - if (f->kind==FE_N_FN && f->text && strcmp(f->text,"drop")==0) - t->has_drop=1; - for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) ++fields; - t->field_count=fields; - if (fields) { - t->fields=(FeFieldType *)fe_arena_alloc(&c->arena, - fields*sizeof(FeFieldType)); - if (!t->fields) { t->field_count=0; return t; } - push_instance_bindings(c,&save,t); - bind_self(c,t); - i=0; - for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) { - t->fields[i].name=f->text; - t->fields[i].type=node_type(c,f->a); - t->fields[i].offset=0; - t->fields[i].ast_node=f; - ++i; - } - pop_bindings(c,&save); - } - fe_type_layout_all(&c->types); - /* A type that says how to let go of itself needs that method to exist for - every instance, whether or not anyone calls it by name: scope cleanup - will. */ - { - FeNode *release; - for (release=decl->children;release;release=release->next) - if (release->kind==FE_N_FN && release->text && - !strcmp(release->text,"drop") && release->c) { - FeCheckerState s; - memset(&s,0,sizeof s); - s.c=c; - s.scope=c->unit_scope[unit_index(c,home)]; - s.globals=s.scope; - check_instance_method(&s,t,release,decl->loc,0); - break; - } - } - return t; -} - -static FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, - FeType **args, unsigned count, FeLoc loc) -{ - FeNode *decl=unit_type_decl(c,home,name); - char key[FE_GENERIC_KEY_MAX]; - if (!decl || !decl_is_generic(decl)) { - err(c,loc,"type does not take generic arguments"); - return unknown(c); - } - if (decl->kind!=FE_N_STRUCT) { - err(c,loc,"only a generic struct can be instantiated"); - return unknown(c); - } - if (count!=decl_type_param_count(decl)) { - err(c,loc,"wrong number of generic arguments"); - return unknown(c); - } - instance_key(key,home->name,name,args,count); - if (instance_record(c,key,loc,decl,home,0)<0) return unknown(c); - return build_struct_instance(c,home,decl,key,args,count); -} - -/* `Name(args...)` written in type position. */ -static FeType *instantiate_type_node(void *owner, const FeNode *node) -{ - FeCheck *c=(FeCheck *)owner; - FeUnit *home=current_unit(c); - const char *name=node->text; - FeNode *arg; - FeType *args[FE_TYPE_PARAM_MAX]; - unsigned count=0; - FeType *result; - /* `binding.Name` names a type in another unit. The binding is not itself a - type, so it has to be peeled off before anything is looked up. */ - if (node->a && node->a->kind==FE_N_IDENT && node->a->text && c->build && - c->unit) { - FeUnit *bound=fe_build_binding(c->build,c->unit,node->text); - if (bound) { home=bound; name=node->a->text; } - } - if (!node->children) { - FeNode *decl=unit_type_decl(c,home,name ? name : ""); - if (decl && decl_is_generic(decl)) { - /* A generic declaration is not a type until it has arguments. */ - err(c,node->loc,"generic type requires type arguments"); - return unknown(c); - } - if (name!=node->text) { - FeType *there=unit_type(c,home,name); - if (there) return there; - } - return fe_type_intern(&c->types,name); - } - if (!instance_descend(c,node->loc)) return unknown(c); - for (arg=node->children;arg;arg=arg->next) { - if (counttypes,arg); - ++count; - } - if (count>FE_TYPE_PARAM_MAX) { - err(c,node->loc,"wrong number of generic arguments"); - --c->instance_depth; - return unknown(c); - } - result=instantiate_struct(c,home,name ? name : "",args,count, - node->loc); - --c->instance_depth; - return result; -} - -/* A type written where an expression is: `i32`, `Box(i32)`. Only a comptime - argument position accepts one. */ -static FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) -{ - FeCheck *c=s->c; - FeType *t; - unsigned i; - *ok=0; - if (!n) return unknown(c); - if (n->kind==FE_N_IDENT && n->text) { - for (i=0;itypes.param_count;++i) - if (strcmp(c->types.params[i].name,n->text)==0) { - *ok=1; - return c->types.params[i].type; - } - if (find_symbol(s->scope,n->text)) { - /* A const alias of a type is that type (SPEC 4.7). */ - FeSym *sym=find_symbol(s->scope,n->text); - if (sym && sym->decl && sym->decl->kind==FE_N_CONST && - sym->decl->b && sym->decl->b->kind==FE_N_IDENT) - return type_from_expr(s,sym->decl->b,ok); - return unknown(c); - } - t=fe_type_intern(&c->types,n->text); - if (t && t->kind!=FE_TYPE_UNKNOWN) { *ok=1; return t; } - return unknown(c); - } - if (n->kind==FE_N_CALL && n->a && - (n->a->kind==FE_N_IDENT || - (n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->b && n->a->b->text))) { - FeType *args[FE_TYPE_PARAM_MAX]; - unsigned count=0; - FeNode *arg; - FeType *result; - FeUnit *home=current_unit(c); - const char *want; - /* `Name(args)` here, `binding.Name(args)` when the declaration is in - another unit. */ - if (n->a->kind==FE_N_MEMBER) { - FeUnit *bound=binding_unit(s,n->a->a); - if (!bound) return unknown(c); - home=bound; - want=n->a->b->text; - } else { - want=n->a->text; - } - if (!want || !unit_type_decl(c,home,want)) return unknown(c); - if (!instance_descend(c,n->loc)) { *ok=1; return unknown(c); } - for (arg=n->children;arg;arg=arg->next) { - int inner=0; - if (countinstance_depth; return unknown(c); } - ++count; - } - if (count>FE_TYPE_PARAM_MAX) { --c->instance_depth; return unknown(c); } - result=instantiate_struct(c,home,want,args,count,n->loc); - --c->instance_depth; - *ok=1; - return result; - } - return unknown(c); -} - -/* A `comptime if` condition. Only the forms SPEC 9 allows: type equality and - the type predicates. Anything else is not decidable here. */ -static int comptime_condition(FeCheckerState *s, FeNode *n, int *out) -{ - FeType *a; - FeType *b; - int ok=0; - int eq; - if (!n) return 0; - if (n->kind==FE_N_BINARY && n->text && - (strcmp(n->text,"==")==0 || strcmp(n->text,"!=")==0)) { - a=type_from_expr(s,n->a,&ok); - if (!ok) return 0; - b=type_from_expr(s,n->b,&ok); - if (!ok) return 0; - eq=fe_type_equal(a,b); - *out=strcmp(n->text,"==")==0 ? eq : !eq; - return 1; - } - if (n->kind==FE_N_CALL && n->text && - (strcmp(n->text,"@is_int")==0 || strcmp(n->text,"@is_ptr")==0)) { - a=type_from_expr(s,n->children,&ok); - if (!ok) return 0; - *out=strcmp(n->text,"@is_int")==0 ? fe_type_is_integer(a) : - (a && (a->kind==FE_TYPE_OWNED || a->kind==FE_TYPE_REF)); - return 1; - } - return 0; -} - -/* Check a generic body once, in the unit that declared it and with the - instance's arguments bound. Errors land on the operation that is wrong; the - call site gets a note, because the call is context and not the defect. */ -static void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, - FeType *owner, FeBindSave *bindings, FeLoc site) -{ - FeAst *save_ast=c->ast; - FeUnit *save_unit=c->unit; - const char *save_name=c->types.unit_name; - unsigned before=c->diags->errors; - (void)bindings; - c->ast=&home->ast; - c->unit=home; - c->types.unit_name=home->name; - fe_diags_source(c->diags,home->source,home->size); - if (owner) check_method(c,decl,c->unit_scope[unit_index(c,home)],owner); - else check_fn(c,decl,c->unit_scope[unit_index(c,home)]); - c->ast=save_ast; - c->unit=save_unit; - c->types.unit_name=save_name; - if (save_unit) fe_diags_source(c->diags,save_unit->source,save_unit->size); - if (c->diags->errors>before) - fe_diag_note_src(c->diags,site,"instantiated here"); -} - -/* A call to a generic function: read the type arguments, check the value - arguments against the bound signature, then check the body once. */ -static FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, - FeUnit *home) -{ - FeCheck *c=s->c; - FeNode *decl=sym->fn; - unsigned want=decl_type_param_count(decl); - FeType *args[FE_TYPE_PARAM_MAX]; - FeNode *arg=n->children; - unsigned i; - char key[FE_GENERIC_KEY_MAX]; - FeBindSave save; - FeType *result; - int fresh; - if (want>FE_TYPE_PARAM_MAX) { - err(c,n->loc,"too many generic parameters"); - return unknown(c); - } - for (i=0;iloc,"generic call requires explicit type arguments"); - return unknown(c); - } - args[i]=type_from_expr(s,arg,&ok); - if (!ok) { - err(c,arg->loc,"a comptime type argument must name a type"); - return unknown(c); - } - arg=arg->next; - } - instance_key(key,home->name,decl->text,args,want); - push_bindings(c,&save,decl,args,want); - result=check_call_args(s,n,sym,home->name,want); - fresh=instance_record(c,key,n->loc,decl,home,0); - pop_bindings(c,&save); - /* The call goes to this instance, not to the declaration it came from. */ - if (n->a) n->a->cname=(char *)instance_cname(c,key); - if (fresh>0) { - if (!instance_descend(c,n->loc)) return result; - push_bindings(c,&save,decl,args,want); - instantiate_body(c,home,decl,0,&save,n->loc); - pop_bindings(c,&save); - --c->instance_depth; - } - return result; -} - -/* `Type.method(...)` where Type is a generic instance and the method takes no - self parameter. */ -/* The unit a name belongs to, by name. */ -static FeUnit *unit_named(FeCheck *c, const char *name) -{ - unsigned u; - if (!name) return 0; - for (u=0;ubuild->count;++u) - if (!strcmp(c->build->units[u].name,name)) return &c->build->units[u]; - return 0; -} - -static FeType *check_static_method_call(FeCheckerState *s, FeNode *n, - FeType *owner, FeNode *method) -{ - FeCheck *c=s->c; - /* A method belongs to the unit that declared its type, not to whichever - unit happens to be calling it. */ - FeUnit *home=unit_named(c,owner ? owner->unit : 0); - FeBindSave save; - FeType *result; - char key[FE_GENERIC_KEY_MAX]; - FeType *self_args[1]; - int fresh; - FeSym fake; - if (!home) home=current_unit(c); - self_args[0]=owner; - instance_key(key,home->name,method->text,self_args,1); - memset(&fake,0,sizeof fake); - fake.name=method->text; - fake.cname=method->cname; - fake.fn=method; - fake.decl=method; - push_instance_bindings(c,&save,owner); - bind_self(c,owner); - result=check_call_args(s,n,&fake,home->name,0); - fresh=instance_record(c,key,n->loc,method,home,owner); - pop_bindings(c,&save); - if (n->a) n->a->cname=(char *)instance_cname(c,key); - if (fresh>0) { - if (!instance_descend(c,n->loc)) return result; - push_instance_bindings(c,&save,owner); - bind_self(c,owner); - instantiate_body(c,home,method,owner,&save,n->loc); - pop_bindings(c,&save); - --c->instance_depth; - } - return result; -} - -/* The body of a method on a generic instance, checked once per instance. */ -static void check_instance_method(FeCheckerState *s, FeType *owner, - FeNode *method, FeLoc site, FeNode *call) -{ - FeCheck *c=s->c; - /* A method belongs to the unit that declared its type, not to whichever - unit happens to be calling it. */ - FeUnit *home=unit_named(c,owner ? owner->unit : 0); - FeBindSave save; - char key[FE_GENERIC_KEY_MAX]; - FeType *self_args[1]; - self_args[0]=owner; - if (!home) home=current_unit(c); - instance_key(key,home->name,method->text,self_args,1); - { - FeBindSave probe; - int fresh; - push_instance_bindings(c,&probe,owner); - bind_self(c,owner); - fresh=instance_record(c,key,site,method,home,owner); - pop_bindings(c,&probe); - /* The call names this instance's copy of the method. */ - if (call && call->a) call->a->cname=(char *)instance_cname(c,key); - if (fresh<=0) return; - } - if (!instance_descend(c,site)) return; - push_instance_bindings(c,&save,owner); - bind_self(c,owner); - instantiate_body(c,home,method,owner,&save,site); - pop_bindings(c,&save); - --c->instance_depth; -} - -/* SPEC 4.7: `const Word = i32;` is another spelling of a type, not a value. - It has no initializer to check and no storage. */ -static int const_names_type(FeCheckerState *s, FeNode *n) -{ - FeType *t; - if (!n->b || n->b->kind!=FE_N_IDENT || !n->b->text) return 0; - if (n->a) return 0; - if (find_symbol(s->globals,n->b->text)) return 0; - t=fe_type_intern(&s->c->types,n->b->text); - return t && t->kind!=FE_TYPE_UNKNOWN; -} - -static FeNode *type_method(FeType *t, const char *name) -{ - FeNode *m; - if (!t || !t->decl_node || !name) return 0; - for (m=t->decl_node->children;m;m=m->next) - if (m->kind==FE_N_FN && m->text && strcmp(m->text,name)==0) return m; - return 0; -} - -static int method_is_static(const FeNode *method) -{ - FeNode *first=method && method->a ? method->a->children : 0; - return !first || !first->text || strcmp(first->text,"self")!=0; -} - -/* A call to a named function. `home` is the unit the signature was written in, - null when that is the unit being checked: parameter and return types have to - be read where they were written or a name would mean the caller's type. */ -/* `error.Name` is a member of the default error set. That set is open -- names - are collected across the build and numbered later, not declared -- so any - name is well formed here and the value's type is core.Error. */ -static int is_error_set_member(FeCheckerState *s, FeNode *n) -{ - return n && n->kind==FE_N_MEMBER && n->a && n->a->kind==FE_N_IDENT && - n->a->text && strcmp(n->a->text,"error")==0 && - n->b && n->b->text && !find_symbol(s->scope,"error"); -} - -/* `binding.name` used as a value rather than called. */ -static FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled) -{ - FeUnit *home=binding_unit(s,n->a); - FeSym *sym; - *handled=0; - if (!home) return 0; - *handled=1; - sym=unit_member(s->c,home,n->b && n->b->text ? n->b->text : ""); - if (!sym) { err(s->c,n->loc,"unknown name"); return unknown(s->c); } - if (!decl_is_public(sym->decl)) { - err(s->c,n->loc,"name is private to its unit"); - return unknown(s->c); - } - n->cname=sym->cname; - n->sem_decl=sym->decl; - n->sem_type=sym->type; - return sym->type; -} - -/* `skip` leading parameters and arguments have already been consumed as - comptime type arguments. */ -static FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, - const char *home, unsigned skip) -{ - FeCheck *c=s->c; - FeNode *param; - FeNode *arg; - FeType *a; - FeType *b; - unsigned k; - if (n->a) n->a->cname = sym->cname; - n->sem_decl = sym->fn; - if (!sym->fn) { - err(c, n->loc, "name is not a function"); - return unknown(c); - } - param = sym->fn->a ? sym->fn->a->children : 0; - arg = n->children; - for (k=0;knext; - if (arg) arg=arg->next; - } - while (param && arg) { - a = check_expr(s, arg); - b = node_type_in(c, home, param->a); - if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && - a->kind==FE_TYPE_REF && a->ref_mut) { - FeSym *root=own_root_symbol(s,arg); - if (root && root->borrow_root) root=root->borrow_root; - if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); - } else if (b && a && b->kind==FE_TYPE_SLICE && !b->ref_mut && - a->kind==FE_TYPE_SLICE && a->ref_mut) { - /* Call-only []mut -> [] weakening is a temporary view. */ - } else if (call_reborrows(b, a)) { - /* Handing an exclusive borrow to a call lends it for the length of - that call and takes it back after: the caller cannot touch it - meanwhile, so nothing is aliased. Without this an exclusive - parameter could be passed onwards exactly once. */ - } else mark_moved(s,arg,a); - if (!compatible(b, a, arg) && - !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - a->kind != FE_TYPE_UNKNOWN) - err(c, arg->loc, "argument type mismatch"); - own_release_temporary_borrow(s,arg); - param = param->next; - arg = arg->next; - } - if (param || arg) err(c, n->loc, "wrong number of arguments"); - a = sym->fn->b ? node_type_in(c, home, sym->fn->b) : - fe_type_intern(&c->types, "void"); - n->sem_type = a; - return a; -} - -static FeType *check_call(FeCheckerState *s, FeNode *n) -{ - FeCheck *c; - FeNode *arg; - FeNode *value; - FeNode *param; - FeSym *sym; - FeType *a; - FeType *b; - FeType *expected; - c=s->c; - if (n->a && n->a->kind==FE_N_IDENT && n->a->text && - strcmp(n->a->text,"Some")==0) { - err(c,n->loc,"Some is only valid as an optional pattern"); - n->sem_type=unknown(c); - return n->sem_type; - } - if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && - n->a->a->kind==FE_N_IDENT && n->a->a->text && - strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { - arg=n->children; - if (strcmp(n->a->b->text,"replace")==0) { - value=arg ? arg->next : 0; - if (!arg || !value || value->next) { - err(c,n->loc,"mem.replace requires destination and value"); - n->sem_type=unknown(c); - return n->sem_type; - } - a=check_expr(s,arg); - if (!a || a->kind!=FE_TYPE_REF || !a->ref_mut || - !arg->a || !lvalue_writable(s,arg->a)) - err(c,n->loc,"mem.replace destination must be a mutable place"); - expected=a && a->kind==FE_TYPE_REF ? a->elem : 0; - b=m7_check_expected(s,value,expected); - if (expected && !fe_type_equal(expected,b) && - !m7_actual_compatible(expected,b,value)) - err(c,value->loc,"mem.replace value type mismatch"); - mark_moved(s,value,value->sem_type ? value->sem_type : b); - n->sem_type=expected ? expected : unknown(c); - fe_type_require_replace(&c->types,n->sem_type); - return n->sem_type; - } - if (strcmp(n->a->b->text,"destroy")==0) { - a=arg ? check_expr(s,arg) : unknown(c); - if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) - err(c,n->loc,"mem.destroy requires exactly one owned pointer"); - else mark_moved(s,arg,a); - n->sem_type=fe_type_intern(&c->types,"void"); - return n->sem_type; - } - if (strcmp(n->a->b->text,"create")==0 || - strcmp(n->a->b->text,"alloc_slice")==0) - return check_expr_core(s,n); - } - if (n->a && n->a->kind==FE_N_IDENT) { - sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); - if (!sym || !sym->fn) { - err(c,n->loc,"unknown function"); - n->sem_type=unknown(c); - return n->sem_type; - } - if (decl_is_generic(sym->fn)) - return check_generic_call(s,n,sym,current_unit(c)); - n->a->cname=sym->cname; - n->sem_decl=sym->fn; - param=sym->fn->a ? sym->fn->a->children : 0; - arg=n->children; - while (param && arg) { - b=node_type(c,param->a); - a=m7_check_expected(s,arg,b); - if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && - a->kind==FE_TYPE_REF && a->ref_mut) { - FeSym *root; - root=own_root_symbol(s,arg); - if (root && root->borrow_root) root=root->borrow_root; - if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); - } else if (!(b && a && b->kind==FE_TYPE_SLICE && - a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut) && - !call_reborrows(b, a)) - mark_moved(s,arg,arg->sem_type ? arg->sem_type : a); - if (!fe_type_equal(b,a) && !m7_actual_compatible(b,a,arg) && - !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && - !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && - a->kind!=FE_TYPE_UNKNOWN) - err(c,arg->loc,"argument type mismatch"); - own_release_temporary_borrow(s,arg); - param=param->next; - arg=arg->next; - } - if (param || arg) err(c,n->loc,"wrong number of arguments"); - n->sem_type=sym->fn->b ? node_type(c,sym->fn->b) : - fe_type_intern(&c->types,"void"); - return n->sem_type; - } - return check_expr_core(s,n); -} - -static void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, - FeOwnState **own, FeFlowBorrow **borrow, - unsigned *count) -{ - *count=flow_capture(s->scope,slots,FE_M7_FLOW_CAP); - *own=flow_own_new(s,*count); - *borrow=flow_borrow_new(s,*count); - flow_own_capture(slots,*own,*count); - flow_borrow_capture(slots,*borrow,*count); -} - -static void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, - FeFlowBorrow *borrow, unsigned count) -{ - flow_restore(slots,count); - flow_own_restore(slots,own,count); - flow_borrow_restore(slots,borrow,count); -} - -static void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, - FeOwnState *own_base, - FeFlowBorrow *borrow_base, - unsigned count, FeFlowSlot *rhs, - FeOwnState *own_rhs, - FeFlowBorrow *borrow_rhs) -{ - (void)s; - flow_merge(base,base,rhs,count); - flow_own_merge(base,own_base,own_rhs,count); - flow_borrow_merge(base,borrow_base,borrow_rhs,count); -} - -static int m7_stmt_definitely_exits(FeNode *n) -{ - FeNode *last; - if (!n) return 0; - if (n->kind==FE_N_RETURN || n->kind==FE_N_BREAK || - n->kind==FE_N_CONTINUE) return 1; - if (n->kind==FE_N_BLOCK) { - last=n->children; - if (!last) return 0; - while (last->next) last=last->next; - return m7_stmt_definitely_exits(last); - } - if (n->kind==FE_N_IF && n->b && n->c) - return m7_stmt_definitely_exits(n->b) && - m7_stmt_definitely_exits(n->c); - return 0; -} - -static FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, - FeM7LazyKind kind) -{ - FeType *left_type; - FeType *payload; - FeType *right_type; - FeFlowSlot base[FE_M7_FLOW_CAP]; - FeFlowSlot rhs[FE_M7_FLOW_CAP]; - FeOwnState *own_base; - FeOwnState *own_rhs; - FeFlowBorrow *borrow_base; - FeFlowBorrow *borrow_rhs; - unsigned count; - unsigned rhs_count; - FeScope *old; - FeType *error_type; - left_type=check_expr(s,n->a); - if (kind==FE_M7_LAZY_ORELSE) { - if (!left_type || left_type->kind!=FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"orelse requires an optional left operand"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - payload=left_type->elem; - if (!fe_own_is_copy_type(payload)) { - if (m7_place_is_projection(n->a)) - err(s->c,n->loc, - "non-Copy optional projection requires mem.replace before orelse"); - else - mark_moved(s,n->a,left_type); - } - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - right_type=m7_check_expected(s,n->b,payload); - if (!fe_type_equal(payload,right_type) && - !m7_actual_compatible(payload,right_type,n->b)) - err(s->c,n->b ? n->b->loc : n->loc,"orelse fallback type mismatch"); - mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); - rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); - own_rhs=flow_own_new(s,rhs_count); - borrow_rhs=flow_borrow_new(s,rhs_count); - flow_own_capture(rhs,own_rhs,rhs_count); - flow_borrow_capture(rhs,borrow_rhs,rhs_count); - if (rhs_count==count) - m7_merge_rhs_flow(s,base,own_base,borrow_base,count, - rhs,own_rhs,borrow_rhs); - n->sem_type=payload; - return payload; - } - if (!left_type || left_type->kind!=FE_TYPE_ERROR_UNION) { - err(s->c,n->loc,"catch requires an error result"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - payload=left_type->error_value; - mark_moved(s,n->a,left_type); - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - if (n->c) { - old=s->scope; - s->scope=scope_new(s,old); - error_type=fe_m7_error_type(&s->c->types,left_type); - if (n->b && n->b->text) - add_symbol(s,s->scope,n->b->text,error_type,0,0,1, - local_cname(s->c,n->b->text),n->b); - check_stmt(s,n->c); - s->scope=old; - if (payload && payload->kind!=FE_TYPE_VOID && - !m7_stmt_definitely_exits(n->c)) - err(s->c,n->loc, - "catch block for a value result must exit instead of falling through"); - if (payload && payload->kind==FE_TYPE_VOID) { - rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); - own_rhs=flow_own_new(s,rhs_count); - borrow_rhs=flow_borrow_new(s,rhs_count); - flow_own_capture(rhs,own_rhs,rhs_count); - flow_borrow_capture(rhs,borrow_rhs,rhs_count); - if (rhs_count==count) - m7_merge_rhs_flow(s,base,own_base,borrow_base,count, - rhs,own_rhs,borrow_rhs); - } else { - m7_restore_flow(base,own_base,borrow_base,count); - } - n->sem_type=payload; - return payload; - } - right_type=m7_check_expected(s,n->b,payload); - if (!fe_type_equal(payload,right_type) && - !m7_actual_compatible(payload,right_type,n->b)) - err(s->c,n->b ? n->b->loc : n->loc,"catch fallback type mismatch"); - mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); - rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); - own_rhs=flow_own_new(s,rhs_count); - borrow_rhs=flow_borrow_new(s,rhs_count); - flow_own_capture(rhs,own_rhs,rhs_count); - flow_borrow_capture(rhs,borrow_rhs,rhs_count); - if (rhs_count==count) - m7_merge_rhs_flow(s,base,own_base,borrow_base,count, - rhs,own_rhs,borrow_rhs); - n->sem_type=payload; - return payload; -} - -static FeType *check_expr(FeCheckerState *s, FeNode *n) -{ - FeType *a; - FeType *b; - FeType *ret_error; - FeType *got_error; - FeM7LazyKind lazy; - const char *op; - if (!n) return unknown(s->c); - if (fe_m7_is_null(n)) { - err(s->c,n->loc,"null requires a contextual optional type"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - if (n->kind==FE_N_IDENT) - return check_identifier(s,n); - if (n->kind==FE_N_LITERAL) - return check_expr_core(s,n); - if (n->kind==FE_N_CALL) - return check_call(s,n); - if (n->kind==FE_N_MEMBER) { - int handled; - FeType *cross; - if (is_error_set_member(s,n)) { - n->sem_type=fe_type_intern(&s->c->types,"core.Error"); - return n->sem_type; - } - cross=cross_unit_value(s,n,&handled); - if (handled) return cross; - a=check_expr(s,n->a); - return m7_member_field(s,n,a); - } - if (n->kind==FE_N_INDEX) - return check_index(s,n); - if (n->kind==FE_N_UNARY) { - op=n->text ? n->text : ""; - if (strcmp(op,"try")==0) { - a=check_expr(s,n->a); - if (!a || a->kind!=FE_TYPE_ERROR_UNION) { - err(s->c,n->loc,"try requires an error result"); - n->sem_type=unknown(s->c); - return n->sem_type; - } - if (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION) { - err(s->c,n->loc,"try requires an enclosing error result"); - } else { - ret_error=fe_m7_error_type(&s->c->types,s->ret); - got_error=fe_m7_error_type(&s->c->types,a); - if (!ret_error || !got_error || !fe_type_equal(ret_error,got_error)) - err(s->c,n->loc, - "try error type must exactly match the enclosing error result"); - } - mark_moved(s,n->a,a); - n->sem_type=a->error_value; - return n->sem_type; - } - if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { - a=check_expr(s,n->a); - own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); - n->sem_type=fe_type_ref(&s->c->types,a,strcmp(op,"&mut")==0); - return n->sem_type; - } - return check_expr_core(s,n); - } - if (n->kind==FE_N_BINARY) { - lazy=fe_m7_lazy_kind(n); - if (lazy!=FE_M7_LAZY_NONE) - return m7_check_lazy(s,n,lazy); - op=n->text ? n->text : ""; - if ((strcmp(op,"==")==0 || strcmp(op,"!=")==0) && - (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { - FeNode *nonnull; - FeNode *nullnode; - nonnull=fe_m7_is_null(n->a) ? n->b : n->a; - nullnode=fe_m7_is_null(n->a) ? n->a : n->b; - a=check_expr(s,nonnull); - if (!a || a->kind!=FE_TYPE_OPTIONAL) - err(s->c,n->loc,"null comparison requires an optional value"); - else { - nullnode->sem_type=a; - nullnode->sem_context=a; - } - n->sem_type=fe_type_intern(&s->c->types,"bool"); - return n->sem_type; - } - a=check_expr(s,n->a); - b=check_expr(s,n->b); - if (strcmp(op,"and")==0 || strcmp(op,"or")==0) { - if ((known(a) && a->kind!=FE_TYPE_BOOL) || - (known(b) && b->kind!=FE_TYPE_BOOL)) - err(s->c,n->loc,"logical operator requires bool operands"); - n->sem_type=fe_type_intern(&s->c->types,"bool"); - return n->sem_type; - } - if (strcmp(op,"==")==0 || strcmp(op,"!=")==0 || - strcmp(op,"<")==0 || strcmp(op,"<=")==0 || - strcmp(op,">")==0 || strcmp(op,">=")==0) { - if (known(a) && known(b) && !fe_type_equal(a,b) && - !m7_actual_compatible(a,b,n->b) && - !m7_actual_compatible(b,a,n->a)) - err(s->c,n->loc,"comparison operands have different types"); - /* Only numbers and characters have an order. */ - else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && - ((known(a) && !ordered_type(a)) || - (known(b) && !ordered_type(b)))) - err(s->c,n->loc,"ordering requires integer or char operands"); - n->sem_type=fe_type_intern(&s->c->types,"bool"); - return n->sem_type; - } - if ((known(a) && !fe_type_is_integer(a)) || - (known(b) && !fe_type_is_integer(b)) || - (known(a) && known(b) && !fe_type_equal(a,b) && - !m7_actual_compatible(a,b,n->b) && - !m7_actual_compatible(b,a,n->a))) - err(s->c,n->loc,"arithmetic operands must have the same integer type"); - n->sem_type=a; - return a; - } - if (n->kind==FE_N_TYPE && n->text && strcmp(n->text,"as")==0) - return check_expr_core(s,n); - if (n->kind==FE_N_STRUCT_INIT) - return check_struct_init(s,n); - if (n->kind==FE_N_ARRAY_INIT) - return check_array_init(s,n); - return check_expr_core(s,n); -} - -static FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) -{ - FeType *base=0; - FeFieldType *field; - FeType *owner; - if (!n) return unknown(s->c); - if (n->kind==FE_N_MEMBER) { - base=check_expr(s,n->a); - if (base && base->kind==FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"optional value must be projected with '.?' first"); - return unknown(s->c); - } - if (base && base->kind==FE_TYPE_REF && n->b && n->b->text && - strcmp(n->b->text,"^")==0) { - if (!base->ref_mut) - err(s->c,n->loc,"cannot write through shared reference"); - n->sem_type=base->elem; - return base->elem; - } - owner=base; - if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && - base->elem && base->elem->kind==FE_TYPE_STRUCT) - owner=base->elem; - if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { - if (base->kind==FE_TYPE_REF && !base->ref_mut) - err(s->c,n->loc,"cannot write through shared reference"); - /* Writing a field still needs a writable place. This branch used to - be reached only by units mentioning M7 syntax, so it never had to - repeat the check the M6 path does. */ - if (base->kind!=FE_TYPE_REF && base->kind!=FE_TYPE_OWNED && - !lvalue_writable(s,n->a)) - err(s->c,n->loc,"cannot assign through immutable value"); - field=fe_type_field(owner,n->b->text); - if (!field) { - err(s->c,n->loc,"assignment requires a valid struct field"); - return unknown(s->c); - } - n->sem_type=field->type; - return field->type; - } - } - return check_lvalue_core(s,n,read,n->kind==FE_N_MEMBER ? base : 0); -} - -static FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, - FeNode *source, int *borrow_mut) -{ - FeSym *root; - int mutable; - *borrow_mut=0; - if (fe_own_is_copy_type(payload)) return payload; - root=own_root_symbol(s,source); - mutable=root && root->mutable; - *borrow_mut=mutable; - if (payload->kind==FE_TYPE_OWNED && payload->elem) - return fe_type_ref(&s->c->types,payload->elem,mutable); - return fe_type_ref(&s->c->types,payload,mutable); -} - -static void m7_check_if_let(FeCheckerState *s, FeNode *n) -{ - FeType *opt; - FeType *binding_type; - FeNode *binding; - FeSym *root; - FeScope *old; - FeFlowSlot base[FE_M7_FLOW_CAP]; - FeFlowSlot left[FE_M7_FLOW_CAP]; - FeFlowSlot right[FE_M7_FLOW_CAP]; - FeOwnState *own_base; - FeOwnState *own_left; - FeOwnState *own_right; - FeFlowBorrow *borrow_base; - FeFlowBorrow *borrow_left; - FeFlowBorrow *borrow_right; - unsigned count; - unsigned i; - int borrow_mut; - int is_some; - opt=check_expr(s,n->a); - if (!opt || opt->kind!=FE_TYPE_OPTIONAL) { - err(s->c,n->loc,"if let Some/None requires an optional value"); - return; - } - is_some=n->aux_text && strcmp(n->aux_text,"Some")==0; - if (!is_some && (!n->aux_text || strcmp(n->aux_text,"None")!=0)) - err(s->c,n->loc,"if let optional pattern must be Some or None"); - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - old=s->scope; - s->scope=scope_new(s,old); - root=0; - borrow_mut=0; - binding=n->children; - if (is_some && binding) { - binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); - add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, - local_cname(s->c,binding->text),binding); - if (!fe_own_is_copy_type(opt->elem)) { - root=own_root_symbol(s,n->a); - if (root) - fe_own_access(s->c->diags,&root->own, - borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, - n->loc); - } - } - check_stmt(s,n->b); - if (root) { - if (borrow_mut) fe_own_release_exclusive(&root->own); - else fe_own_release_shared(&root->own); - } - s->scope=old; - flow_capture(s->scope,left,count); - own_left=flow_own_new(s,count); - borrow_left=flow_borrow_new(s,count); - flow_own_capture(left,own_left,count); - flow_borrow_capture(left,borrow_left,count); - m7_restore_flow(base,own_base,borrow_base,count); - if (n->c) check_stmt(s,n->c); - if (n->c) { - flow_capture(s->scope,right,count); - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - flow_own_capture(right,own_right,count); - flow_borrow_capture(right,borrow_right,count); - } else { - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - for (i=0;ichildren;arm;arm=arm->next) { - m7_restore_flow(base,own_base,borrow_base,count); - old=s->scope; - s->scope=scope_new(s,old); - root=0; - borrow_mut=0; - if (arm->text && strcmp(arm->text,"Some")==0) { - if (seen_some) err(s->c,arm->loc,"duplicate Some match arm"); - seen_some=1; - binding=arm->children; - if (binding) { - binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); - add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, - local_cname(s->c,binding->text),binding); - if (!fe_own_is_copy_type(opt->elem)) { - root=own_root_symbol(s,n->a); - if (root) - fe_own_access(s->c->diags,&root->own, - borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, - arm->loc); - } - } - } else if (arm->text && strcmp(arm->text,"None")==0) { - if (seen_none) err(s->c,arm->loc,"duplicate None match arm"); - seen_none=1; - } else if (arm->text && strcmp(arm->text,"_")==0) { - wildcard=1; - } else { - err(s->c,arm->loc,"optional match arm must be Some, None, or _"); - } - if (arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); - else if (arm->a) check_expr(s,arm->a); - if (root) { - if (borrow_mut) fe_own_release_exclusive(&root->own); - else fe_own_release_shared(&root->own); - } - s->scope=old; - flow_capture(s->scope,current,count); - own_current=flow_own_new(s,count); - borrow_current=flow_borrow_new(s,count); - flow_own_capture(current,own_current,count); - flow_borrow_capture(current,borrow_current,count); - if (!have) { - for (i=0;ic,n->loc,"non-exhaustive optional match"); - if (have) m7_restore_flow(merged,own_merged,borrow_merged,count); -} - -static void m7_check_match_stmt(FeCheckerState *s, FeNode *n) -{ - FeType *value; - value=check_expr(s,n->a); - if (value && value->kind==FE_TYPE_OPTIONAL) { - m7_check_optional_match(s,n,value); - n->sem_type=unknown(s->c); - return; - } - check_match(s,n); -} - -static void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) -{ - FeType *expected; - FeType *actual; - FeType *stored; - FeSym *sym; - int initialized; - expected=n->a ? node_type(s->c,n->a) : 0; - if (n->b) - stored=m7_check_expected(s,n->b,expected); - else - stored=expected ? expected : unknown(s->c); - actual=n->b && n->b->sem_type ? n->b->sem_type : stored; - if (!expected) expected=stored; - if (!n->a && n->b && fe_m7_is_null(n->b)) - err(s->c,n->loc,"null initializer requires an explicit optional type"); - if (expected && expected->kind==FE_TYPE_VOID) - err(s->c,n->loc,"variable cannot have void type"); - if (n->b && !fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->b)) { - /* Say which rule was hit. Weakening &mut to & is a distinct thing from - two unrelated types not matching, and "type mismatch" told the reader - nothing about why the exclusive borrow could not be shared. */ - if (expected && stored && expected->kind==FE_TYPE_REF && - stored->kind==FE_TYPE_REF && !expected->ref_mut && stored->ref_mut) - err(s->c,n->loc, - "cannot rebind a mut borrow as a shared reference"); - else if (expected && stored && expected->kind==FE_TYPE_SLICE && - stored->kind==FE_TYPE_SLICE && !expected->ref_mut && - stored->ref_mut) - err(s->c,n->loc, - "cannot rebind a mut slice as a shared slice"); - else - err(s->c,n->loc,"initializer type mismatch"); - } - /* Rules the M6 declaration case carried that this one has to repeat now - that it is the only declaration case. */ - if (n->b && stored && stored->kind==FE_TYPE_VOID) - err(s->c,n->loc,"void expression cannot initialize a variable"); - if (!mutable && expected && expected->kind==FE_TYPE_SLICE && - expected->ref_mut) - err(s->c,n->loc,"let cannot bind a mutable slice"); - if (!n->b && !n->a) - err(s->c,n->loc,"uninitialized var requires an explicit type"); - if (n->b) mark_moved(s,n->b,actual); - initialized=n->b!=0; - sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, - local_cname(s->c,n->text ? n->text : "local"),n); - if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && - (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { - sym->borrow_root=own_root_symbol(s,n->b->a); - sym->borrow_mut=strcmp(n->b->text,"&mut")==0; - sym->borrow_defer=s->defer_depth!=0 || - own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); - } - own_bind_derived_call(s,sym,n->b); -} - -static void check_stmt(FeCheckerState *s, FeNode *n) -{ - FeScope *old; - FeNode *x; - FeType *expected; - FeType *actual; - FeType *stored; - FeSym *sym; - if (!n) return; - switch (n->kind) { - case FE_N_BLOCK: - old=s->scope; - s->scope=scope_new(s,old); - for (x=n->children;x;x=x->next) { - check_stmt(s,x); - own_release_after_stmt(s,s->scope,x,0); - } - own_release_after_stmt(s,s->scope,n,1); - s->scope=old; - break; - case FE_N_LET: - case FE_N_CONST: - m7_check_decl_stmt(s,n,0); - break; - case FE_N_VAR: - if (!n->a && !n->b) - err(s->c,n->loc,"uninitialized var requires an explicit type"); - m7_check_decl_stmt(s,n,1); - break; - case FE_N_ASSIGN: - expected=check_lvalue(s,n->a,compound_operator(n->text)); - stored=m7_check_expected(s,n->b,expected); - actual=n->b && n->b->sem_type ? n->b->sem_type : stored; - if (!fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->b)) - err(s->c,n->loc,"assignment type mismatch"); - mark_moved(s,n->b,actual); - sym=n->a && n->a->kind==FE_N_IDENT ? - find_symbol(s->scope,n->a->text) : 0; - if (sym && sym->mutable) { - sym->initialized=1; - fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); - sym->moved=sym->own.move; - /* Rebinding a reference, from the M6 assignment case: the new - source has to live at least as long as the reference does, and - the previous borrow has to be released. */ - if (n->b && n->b->kind==FE_N_UNARY && n->b->text && - (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && - fe_own_is_reference_like(sym->type)) { - FeSym *root=own_root_symbol(s,n->b->a); - if (root && root->owner!=sym->owner) - err(s->c,n->b->loc,"reference would outlive its source scope"); - else if (root) { - if (sym->borrow_root) { - if (sym->borrow_mut) - fe_own_release_exclusive(&sym->borrow_root->own); - else fe_own_release_shared(&sym->borrow_root->own); - } - sym->borrow_root=root; - sym->borrow_mut=strcmp(n->b->text,"&mut")==0; - } - } - } - break; - case FE_N_EXPR_STMT: - check_expr(s,n->a); - break; - case FE_N_DEFER: - ++s->defer_depth; - check_stmt(s,n->a); - --s->defer_depth; - break; - case FE_N_IF: - if (n->text && strcmp(n->text,"comptime if")==0) { - int taken=0; - if (!comptime_condition(s,n->a,&taken)) { - err(s->c,n->a?n->a->loc:n->loc, - "comptime condition must be decidable at compile time"); - break; - } - /* SPEC 9: the branch that is not taken is parsed and nothing more. */ - if (taken) check_stmt(s,n->b); - else if (n->c) check_stmt(s,n->c); - break; - } - if (n->text && strcmp(n->text,"if let")==0) - m7_check_if_let(s,n); - else { - FeType *cond; - FeFlowSlot base[FE_M7_FLOW_CAP]; - FeFlowSlot left[FE_M7_FLOW_CAP]; - FeFlowSlot right[FE_M7_FLOW_CAP]; - FeOwnState *own_base; - FeOwnState *own_left; - FeOwnState *own_right; - FeFlowBorrow *borrow_base; - FeFlowBorrow *borrow_left; - FeFlowBorrow *borrow_right; - unsigned count; - unsigned i; - cond=check_expr(s,n->a); - if (known(cond) && cond->kind!=FE_TYPE_BOOL) - err(s->c,n->loc,"if condition must be bool"); - /* Once a function is on the M7 checker path, branch bodies must - remain on that path as well. In particular, return T inside - E!T relies on contextual success construction even when the - branch itself contains no surface M7 syntax. */ - m7_capture_flow(s,base,&own_base,&borrow_base,&count); - check_stmt(s,n->b); - flow_capture(s->scope,left,count); - own_left=flow_own_new(s,count); - borrow_left=flow_borrow_new(s,count); - flow_own_capture(left,own_left,count); - flow_borrow_capture(left,borrow_left,count); - /* A branch that always leaves contributes nothing to what follows. - Merging its state would make a value it consumed look consumed - afterwards, on a path that never ran it. */ - if (m7_stmt_definitely_exits(n->b)) { - for (i=0;ic) check_stmt(s,n->c); - if (n->c) { - flow_capture(s->scope,right,count); - own_right=flow_own_new(s,count); - borrow_right=flow_borrow_new(s,count); - flow_own_capture(right,own_right,count); - flow_borrow_capture(right,borrow_right,count); - if (m7_stmt_definitely_exits(n->c)) { - for (i=0;iret; - if (n->a) - stored=m7_check_expected(s,n->a,expected); - else - stored=fe_type_intern(&s->c->types,"void"); - actual=n->a && n->a->sem_type ? n->a->sem_type : stored; - /* R8, from the M6 return case: a returned reference has to come from a - parameter or a static, never from a local. */ - if (expected && fe_own_is_reference_like(expected) && - !own_return_from_allowed_root(s,n->a)) - err(s->c,n->loc, - "reference return must be derived from a parameter or static"); - if (n->a && stored && stored->kind==FE_TYPE_VOID && - expected && expected->kind!=FE_TYPE_VOID) - err(s->c,n->loc,"void expression returned from value function"); - if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a && - actual && actual->kind==FE_TYPE_ERROR_UNION && - !fe_type_equal(expected,actual)) - err(s->c,n->loc,"error result type mismatch"); - /* A bare `return` in a function returning `!void` is the success case: - there is no value to give, and no error either. */ - else if (!n->a && expected && expected->kind==FE_TYPE_ERROR_UNION && - expected->error_value && - expected->error_value->kind==FE_TYPE_VOID) { } - else if (!fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->a)) - err(s->c,n->loc,"return type mismatch"); - if (n->a) mark_moved(s,n->a,actual); - break; - case FE_N_WHILE: - case FE_N_FOR: - /* The core loop case carries the flow capture and merge that detects a - value moved on every iteration, and it already recurses into the body - through this function, so there is nothing to special-case here. The - M7 half used to skip all of it. */ - check_stmt_core(s,n); - break; - case FE_N_BREAK: - case FE_N_CONTINUE: - if (!s->loop_depth) - err(s->c,n->loc,"break or continue outside loop"); - break; - case FE_N_UNSAFE: - check_stmt(s,n->a); - break; - default: - check_stmt_core(s,n); - break; - } -} - -static int m7_ast_reference_storage(FeNode *type) -{ - if (!type || !type->text) return 0; - if (strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || - (strcmp(type->text,"[")==0 && !type->a) || - strcmp(type->text,"str")==0) - return 1; - if (strcmp(type->text,"?")==0) - return m7_ast_reference_storage(type->a); - if (strcmp(type->text,"^")==0) return 0; - return 0; -} - -static void m7_check_storage(FeCheck *c, FeNode *decl) -{ - FeNode *m; - if (!decl) return; - if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { - /* This pass exists for the shapes the other one cannot see, such as a - reference behind an optional. A plain `&T` field is seen by both, so - leave that one to check_reference_storage below. */ - for (m=decl->children;m;m=m->next) - if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a) && - !own_ast_reference_type(m->a) && - !own_ast_pointer_to_reference(m->a)) - err(c,m->loc,"reference type is not allowed in aggregate storage"); - } - check_reference_storage(c,decl); -} - -static void m7_validate_error_decl(FeCheck *c, FeNode *decl) -{ - FeNode *a; - FeNode *b; - unsigned long code; - unsigned long other; - if (!decl || decl->kind!=FE_N_ERROR_DECL) return; - for (a=decl->children;a;a=a->next) { - if (!a->a || a->a->kind!=FE_N_LITERAL || !a->a->text) continue; - code=strtoul(a->a->text,0,0); - if (code==0UL) - err(c,a->loc,"error code 0 is reserved for success"); - for (b=decl->children;b && b!=a;b=b->next) { - if (a->text && b->text && strcmp(a->text,b->text)==0) { - err(c,a->loc,"duplicate error member name"); - break; - } - if (b->a && b->a->kind==FE_N_LITERAL && b->a->text) { - other=strtoul(b->a->text,0,0); - if (other==code) { - err(c,a->loc,"duplicate error numeric code"); - break; - } - } - } - } -} - -/* Everything a unit declares, before any body anywhere is looked at. */ -static void declare_unit(FeCheck *c) -{ - FeNode *n; - /* A generic declaration is not a type; only its instances are. */ - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) - fe_type_declare_struct(&c->types,n,(n->flags & FE_NODE_PACKED)!=0); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { - FeNode *m; - m7_check_storage(c,n); - if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); - check_generic_params(c,n); - for (m=n->kind==FE_N_STRUCT ? n->children : 0;m;m=m->next) - if (m->kind==FE_N_FN) check_generic_params(c,m); - } - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_ENUM && !decl_is_generic(n)) - fe_type_declare_enum(&c->types,n); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); - check_type_cycles(c); -} - -/* The unit's top-level names, in a scope of their own so that another unit - can look into it later without inheriting anything else. */ -static FeScope *declare_unit_scope(FeCheck *c, FeCheckerState *s) -{ - FeNode *n; - FeNode *m; - FeType *t; - FeScope *globals; - char method_name[128]; - globals=scope_new(s,0); - s->scope=globals; - s->globals=globals; - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { - if (n->kind==FE_N_STRUCT) { - for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) { - sprintf(method_name,"%s_%s",n->text ? n->text : "Type", - m->text ? m->text : "method"); - m->cname=unit_cname(c,method_name); - } - } - if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { - t=n->a ? node_type(c,n->a) : unknown(c); - add_symbol(s,globals,n->text,t,0,n->kind==FE_N_GLOBAL, - n->b!=0,unit_cname(c,n->text ? n->text : "global"),n); - } - } - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN) { - t=fe_type_intern(&c->types,""); - /* `extern "c"` means the linker already knows this name, so it is - not decorated with the unit it was declared in. */ - add_symbol(s,globals,n->text,t,n,0,1, - (n->flags & FE_NODE_EXTERN) && n->text ? n->text : - unit_cname(c,n->text ? n->text : "fn"),n); - } - return globals; -} - -static void check_unit_bodies(FeCheck *c, FeCheckerState *s) -{ - FeNode *n; - FeNode *m; - FeSym *sym; - FeType *t; - FeType *iv; - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { - sym=find_current(s->globals,n->text ? n->text : ""); - if (n->kind==FE_N_CONST && const_names_type(s,n)) continue; - if (n->b) { - iv=m7_check_expected(s,n->b,sym ? sym->type : 0); - if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { - sym->type=iv; - n->sem_type=iv; - } else if (sym && !fe_type_equal(sym->type,iv) && - !m7_actual_compatible(sym->type,iv,n->b)) - err(c,n->loc,"global initializer type mismatch"); - } - } - /* A generic body means nothing until its parameters are bound, so it is - checked once per instance and not here. */ - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_FN && !decl_is_generic(n)) check_fn(c,n,s->globals); - for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) - if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) { - t=fe_type_intern(&c->types,n->text); - for (m=n->children;m;m=m->next) - if (m->kind==FE_N_FN) check_method(c,m,s->globals,t); - } -} - -int fe_check_program(FeCheck *c) -{ - FeCheckerState s; - unsigned u; - s.c=c; - s.scope=0; - s.globals=0; - s.ret=fe_type_intern(&c->types,"void"); - s.loop_depth=0; - s.defer_depth=0; - s.fn_node=0; - fe_own_liveness_init(&s.liveness,&c->arena); - for (u=0;ubuild->count;++u) { enter_unit(c,u); declare_unit(c); } - fe_type_layout_all(&c->types); - for (u=0;ubuild->count;++u) { - enter_unit(c,u); - c->unit_scope[u]=declare_unit_scope(c,&s); - } - for (u=0;ubuild->count;++u) { - enter_unit(c,u); - s.scope=c->unit_scope[u]; - s.globals=c->unit_scope[u]; - check_unit_bodies(c,&s); - } - fe_type_layout_all(&c->types); - return c->diags->errors==0; -} - diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c new file mode 100644 index 0000000..11e1d50 --- /dev/null +++ b/fec/src/checkcal.c @@ -0,0 +1,978 @@ +#include "checkpri.h" + +int is_error_set_member(FeCheckerState *s, FeNode *n) +{ + return n && n->kind==FE_N_MEMBER && n->a && n->a->kind==FE_N_IDENT && + n->a->text && strcmp(n->a->text,"error")==0 && + n->b && n->b->text && !find_symbol(s->scope,"error"); +} + +/* `binding.name` used as a value rather than called. */ +FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled) +{ + FeUnit *home=binding_unit(s,n->a); + FeSym *sym; + *handled=0; + if (!home) return 0; + *handled=1; + sym=unit_member(s->c,home,n->b && n->b->text ? n->b->text : ""); + if (!sym) { err(s->c,n->loc,"unknown name"); return unknown(s->c); } + if (!decl_is_public(sym->decl)) { + err(s->c,n->loc,"name is private to its unit"); + return unknown(s->c); + } + n->cname=sym->cname; + n->sem_decl=sym->decl; + n->sem_type=sym->type; + return sym->type; +} + +/* `skip` leading parameters and arguments have already been consumed as + comptime type arguments. */ +FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home, unsigned skip) +{ + FeCheck *c=s->c; + FeNode *param; + FeNode *arg; + FeType *a; + FeType *b; + unsigned k; + if (n->a) n->a->cname = sym->cname; + n->sem_decl = sym->fn; + if (!sym->fn) { + err(c, n->loc, "name is not a function"); + return unknown(c); + } + param = sym->fn->a ? sym->fn->a->children : 0; + arg = n->children; + for (k=0;knext; + if (arg) arg=arg->next; + } + while (param && arg) { + a = check_expr(s, arg); + b = node_type_in(c, home, param->a); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (b && a && b->kind==FE_TYPE_SLICE && !b->ref_mut && + a->kind==FE_TYPE_SLICE && a->ref_mut) { + /* Call-only []mut -> [] weakening is a temporary view. */ + } else if (call_reborrows(b, a)) { + /* Handing an exclusive borrow to a call lends it for the length of + that call and takes it back after: the caller cannot touch it + meanwhile, so nothing is aliased. Without this an exclusive + parameter could be passed onwards exactly once. */ + } else mark_moved(s,arg,a); + if (!compatible(b, a, arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind != FE_TYPE_UNKNOWN) + err(c, arg->loc, "argument type mismatch"); + own_release_temporary_borrow(s,arg); + param = param->next; + arg = arg->next; + } + if (param || arg) err(c, n->loc, "wrong number of arguments"); + a = sym->fn->b ? node_type_in(c, home, sym->fn->b) : + fe_type_intern(&c->types, "void"); + n->sem_type = a; + return a; +} + +FeType *check_call(FeCheckerState *s, FeNode *n) +{ + FeCheck *c; + FeNode *arg; + FeNode *value; + FeNode *param; + FeSym *sym; + FeType *a; + FeType *b; + FeType *expected; + c=s->c; + if (n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"Some")==0) { + err(c,n->loc,"Some is only valid as an optional pattern"); + n->sem_type=unknown(c); + return n->sem_type; + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { + arg=n->children; + if (strcmp(n->a->b->text,"replace")==0) { + value=arg ? arg->next : 0; + if (!arg || !value || value->next) { + err(c,n->loc,"mem.replace requires destination and value"); + n->sem_type=unknown(c); + return n->sem_type; + } + a=check_expr(s,arg); + if (!a || a->kind!=FE_TYPE_REF || !a->ref_mut || + !arg->a || !lvalue_writable(s,arg->a)) + err(c,n->loc,"mem.replace destination must be a mutable place"); + expected=a && a->kind==FE_TYPE_REF ? a->elem : 0; + b=m7_check_expected(s,value,expected); + if (expected && !fe_type_equal(expected,b) && + !m7_actual_compatible(expected,b,value)) + err(c,value->loc,"mem.replace value type mismatch"); + mark_moved(s,value,value->sem_type ? value->sem_type : b); + n->sem_type=expected ? expected : unknown(c); + fe_type_require_replace(&c->types,n->sem_type); + return n->sem_type; + } + if (strcmp(n->a->b->text,"destroy")==0) { + a=arg ? check_expr(s,arg) : unknown(c); + if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) + err(c,n->loc,"mem.destroy requires exactly one owned pointer"); + else mark_moved(s,arg,a); + n->sem_type=fe_type_intern(&c->types,"void"); + return n->sem_type; + } + if (strcmp(n->a->b->text,"create")==0 || + strcmp(n->a->b->text,"alloc_slice")==0) + return check_expr_core(s,n); + } + if (n->a && n->a->kind==FE_N_IDENT) { + sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); + if (!sym || !sym->fn) { + err(c,n->loc,"unknown function"); + n->sem_type=unknown(c); + return n->sem_type; + } + if (decl_is_generic(sym->fn)) + return check_generic_call(s,n,sym,current_unit(c)); + n->a->cname=sym->cname; + n->sem_decl=sym->fn; + param=sym->fn->a ? sym->fn->a->children : 0; + arg=n->children; + while (param && arg) { + b=node_type(c,param->a); + a=m7_check_expected(s,arg,b); + if (b && a && b->kind==FE_TYPE_REF && !b->ref_mut && + a->kind==FE_TYPE_REF && a->ref_mut) { + FeSym *root; + root=own_root_symbol(s,arg); + if (root && root->borrow_root) root=root->borrow_root; + if (root) fe_own_call_shared_view(c->diags,&root->own,arg->loc); + } else if (!(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut) && + !call_reborrows(b, a)) + mark_moved(s,arg,arg->sem_type ? arg->sem_type : a); + if (!fe_type_equal(b,a) && !m7_actual_compatible(b,a,arg) && + !(b && a && b->kind==FE_TYPE_SLICE && a->kind==FE_TYPE_SLICE && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && fe_type_equal(b->elem,a->elem)) && + a->kind!=FE_TYPE_UNKNOWN) + err(c,arg->loc,"argument type mismatch"); + own_release_temporary_borrow(s,arg); + param=param->next; + arg=arg->next; + } + if (param || arg) err(c,n->loc,"wrong number of arguments"); + n->sem_type=sym->fn->b ? node_type(c,sym->fn->b) : + fe_type_intern(&c->types,"void"); + return n->sem_type; + } + return check_expr_core(s,n); +} + +void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, + FeOwnState **own, FeFlowBorrow **borrow, + unsigned *count) +{ + *count=flow_capture(s->scope,slots,FE_M7_FLOW_CAP); + *own=flow_own_new(s,*count); + *borrow=flow_borrow_new(s,*count); + flow_own_capture(slots,*own,*count); + flow_borrow_capture(slots,*borrow,*count); +} + +void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, + FeFlowBorrow *borrow, unsigned count) +{ + flow_restore(slots,count); + flow_own_restore(slots,own,count); + flow_borrow_restore(slots,borrow,count); +} + +void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, + FeOwnState *own_base, + FeFlowBorrow *borrow_base, + unsigned count, FeFlowSlot *rhs, + FeOwnState *own_rhs, + FeFlowBorrow *borrow_rhs) +{ + (void)s; + flow_merge(base,base,rhs,count); + flow_own_merge(base,own_base,own_rhs,count); + flow_borrow_merge(base,borrow_base,borrow_rhs,count); +} + +int m7_stmt_definitely_exits(FeNode *n) +{ + FeNode *last; + if (!n) return 0; + if (n->kind==FE_N_RETURN || n->kind==FE_N_BREAK || + n->kind==FE_N_CONTINUE) return 1; + if (n->kind==FE_N_BLOCK) { + last=n->children; + if (!last) return 0; + while (last->next) last=last->next; + return m7_stmt_definitely_exits(last); + } + if (n->kind==FE_N_IF && n->b && n->c) + return m7_stmt_definitely_exits(n->b) && + m7_stmt_definitely_exits(n->c); + return 0; +} + +FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, + FeM7LazyKind kind) +{ + FeType *left_type; + FeType *payload; + FeType *right_type; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot rhs[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_rhs; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_rhs; + unsigned count; + unsigned rhs_count; + FeScope *old; + FeType *error_type; + left_type=check_expr(s,n->a); + if (kind==FE_M7_LAZY_ORELSE) { + if (!left_type || left_type->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"orelse requires an optional left operand"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->elem; + if (!fe_own_is_copy_type(payload)) { + if (m7_place_is_projection(n->a)) + err(s->c,n->loc, + "non-Copy optional projection requires mem.replace before orelse"); + else + mark_moved(s,n->a,left_type); + } + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"orelse fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; + } + if (!left_type || left_type->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"catch requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + payload=left_type->error_value; + mark_moved(s,n->a,left_type); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + if (n->c) { + old=s->scope; + s->scope=scope_new(s,old); + error_type=fe_m7_error_type(&s->c->types,left_type); + if (n->b && n->b->text) + add_symbol(s,s->scope,n->b->text,error_type,0,0,1, + local_cname(s->c,n->b->text),n->b); + check_stmt(s,n->c); + s->scope=old; + if (payload && payload->kind!=FE_TYPE_VOID && + !m7_stmt_definitely_exits(n->c)) + err(s->c,n->loc, + "catch block for a value result must exit instead of falling through"); + if (payload && payload->kind==FE_TYPE_VOID) { + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + } else { + m7_restore_flow(base,own_base,borrow_base,count); + } + n->sem_type=payload; + return payload; + } + right_type=m7_check_expected(s,n->b,payload); + if (!fe_type_equal(payload,right_type) && + !m7_actual_compatible(payload,right_type,n->b)) + err(s->c,n->b ? n->b->loc : n->loc,"catch fallback type mismatch"); + mark_moved(s,n->b,n->b && n->b->sem_type ? n->b->sem_type : right_type); + rhs_count=flow_capture(s->scope,rhs,FE_M7_FLOW_CAP); + own_rhs=flow_own_new(s,rhs_count); + borrow_rhs=flow_borrow_new(s,rhs_count); + flow_own_capture(rhs,own_rhs,rhs_count); + flow_borrow_capture(rhs,borrow_rhs,rhs_count); + if (rhs_count==count) + m7_merge_rhs_flow(s,base,own_base,borrow_base,count, + rhs,own_rhs,borrow_rhs); + n->sem_type=payload; + return payload; +} + +FeType *check_expr(FeCheckerState *s, FeNode *n) +{ + FeType *a; + FeType *b; + FeType *ret_error; + FeType *got_error; + FeM7LazyKind lazy; + const char *op; + if (!n) return unknown(s->c); + if (fe_m7_is_null(n)) { + err(s->c,n->loc,"null requires a contextual optional type"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (n->kind==FE_N_IDENT) + return check_identifier(s,n); + if (n->kind==FE_N_LITERAL) + return check_expr_core(s,n); + if (n->kind==FE_N_CALL) + return check_call(s,n); + if (n->kind==FE_N_MEMBER) { + int handled; + FeType *cross; + if (is_error_set_member(s,n)) { + n->sem_type=fe_type_intern(&s->c->types,"core.Error"); + return n->sem_type; + } + cross=cross_unit_value(s,n,&handled); + if (handled) return cross; + a=check_expr(s,n->a); + return m7_member_field(s,n,a); + } + if (n->kind==FE_N_INDEX) + return check_index(s,n); + if (n->kind==FE_N_UNARY) { + op=n->text ? n->text : ""; + if (strcmp(op,"try")==0) { + a=check_expr(s,n->a); + if (!a || a->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an error result"); + n->sem_type=unknown(s->c); + return n->sem_type; + } + if (!s->ret || s->ret->kind!=FE_TYPE_ERROR_UNION) { + err(s->c,n->loc,"try requires an enclosing error result"); + } else { + ret_error=fe_m7_error_type(&s->c->types,s->ret); + got_error=fe_m7_error_type(&s->c->types,a); + if (!ret_error || !got_error || !fe_type_equal(ret_error,got_error)) + err(s->c,n->loc, + "try error type must exactly match the enclosing error result"); + } + mark_moved(s,n->a,a); + n->sem_type=a->error_value; + return n->sem_type; + } + if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + a=check_expr(s,n->a); + own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); + n->sem_type=fe_type_ref(&s->c->types,a,strcmp(op,"&mut")==0); + return n->sem_type; + } + return check_expr_core(s,n); + } + if (n->kind==FE_N_BINARY) { + lazy=fe_m7_lazy_kind(n); + if (lazy!=FE_M7_LAZY_NONE) + return m7_check_lazy(s,n,lazy); + op=n->text ? n->text : ""; + if ((strcmp(op,"==")==0 || strcmp(op,"!=")==0) && + (fe_m7_is_null(n->a) || fe_m7_is_null(n->b))) { + FeNode *nonnull; + FeNode *nullnode; + nonnull=fe_m7_is_null(n->a) ? n->b : n->a; + nullnode=fe_m7_is_null(n->a) ? n->a : n->b; + a=check_expr(s,nonnull); + if (!a || a->kind!=FE_TYPE_OPTIONAL) + err(s->c,n->loc,"null comparison requires an optional value"); + else { + nullnode->sem_type=a; + nullnode->sem_context=a; + } + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + a=check_expr(s,n->a); + b=check_expr(s,n->b); + if (strcmp(op,"and")==0 || strcmp(op,"or")==0) { + if ((known(a) && a->kind!=FE_TYPE_BOOL) || + (known(b) && b->kind!=FE_TYPE_BOOL)) + err(s->c,n->loc,"logical operator requires bool operands"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + if (strcmp(op,"==")==0 || strcmp(op,"!=")==0 || + strcmp(op,"<")==0 || strcmp(op,"<=")==0 || + strcmp(op,">")==0 || strcmp(op,">=")==0) { + if (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a)) + err(s->c,n->loc,"comparison operands have different types"); + /* Only numbers and characters have an order. */ + else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && + ((known(a) && !ordered_type(a)) || + (known(b) && !ordered_type(b)))) + err(s->c,n->loc,"ordering requires integer or char operands"); + n->sem_type=fe_type_intern(&s->c->types,"bool"); + return n->sem_type; + } + if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b)) || + (known(a) && known(b) && !fe_type_equal(a,b) && + !m7_actual_compatible(a,b,n->b) && + !m7_actual_compatible(b,a,n->a))) + err(s->c,n->loc,"arithmetic operands must have the same integer type"); + n->sem_type=a; + return a; + } + if (n->kind==FE_N_TYPE && n->text && strcmp(n->text,"as")==0) + return check_expr_core(s,n); + if (n->kind==FE_N_STRUCT_INIT) + return check_struct_init(s,n); + if (n->kind==FE_N_ARRAY_INIT) + return check_array_init(s,n); + return check_expr_core(s,n); +} + +FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) +{ + FeType *base=0; + FeFieldType *field; + FeType *owner; + if (!n) return unknown(s->c); + if (n->kind==FE_N_MEMBER) { + base=check_expr(s,n->a); + if (base && base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base && base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + if (!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + n->sem_type=base->elem; + return base->elem; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + if (base->kind==FE_TYPE_REF && !base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + /* Writing a field still needs a writable place. This branch used to + be reached only by units mentioning M7 syntax, so it never had to + repeat the check the M6 path does. */ + if (base->kind!=FE_TYPE_REF && base->kind!=FE_TYPE_OWNED && + !lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"assignment requires a valid struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + } + return check_lvalue_core(s,n,read,n->kind==FE_N_MEMBER ? base : 0); +} + +FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, + FeNode *source, int *borrow_mut) +{ + FeSym *root; + int mutable; + *borrow_mut=0; + if (fe_own_is_copy_type(payload)) return payload; + root=own_root_symbol(s,source); + mutable=root && root->mutable; + *borrow_mut=mutable; + if (payload->kind==FE_TYPE_OWNED && payload->elem) + return fe_type_ref(&s->c->types,payload->elem,mutable); + return fe_type_ref(&s->c->types,payload,mutable); +} + +void m7_check_if_let(FeCheckerState *s, FeNode *n) +{ + FeType *opt; + FeType *binding_type; + FeNode *binding; + FeSym *root; + FeScope *old; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + int borrow_mut; + int is_some; + opt=check_expr(s,n->a); + if (!opt || opt->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"if let Some/None requires an optional value"); + return; + } + is_some=n->aux_text && strcmp(n->aux_text,"Some")==0; + if (!is_some && (!n->aux_text || strcmp(n->aux_text,"None")!=0)) + err(s->c,n->loc,"if let optional pattern must be Some or None"); + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + binding=n->children; + if (is_some && binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + n->loc); + } + } + check_stmt(s,n->b); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + m7_restore_flow(base,own_base,borrow_base,count); + if (n->c) check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + } else { + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + for (i=0;ichildren;arm;arm=arm->next) { + m7_restore_flow(base,own_base,borrow_base,count); + old=s->scope; + s->scope=scope_new(s,old); + root=0; + borrow_mut=0; + if (arm->text && strcmp(arm->text,"Some")==0) { + if (seen_some) err(s->c,arm->loc,"duplicate Some match arm"); + seen_some=1; + binding=arm->children; + if (binding) { + binding_type=m7_pattern_binding_type(s,opt->elem,n->a,&borrow_mut); + add_symbol(s,s->scope,binding->text,binding_type,0,borrow_mut,1, + local_cname(s->c,binding->text),binding); + if (!fe_own_is_copy_type(opt->elem)) { + root=own_root_symbol(s,n->a); + if (root) + fe_own_access(s->c->diags,&root->own, + borrow_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + arm->loc); + } + } + } else if (arm->text && strcmp(arm->text,"None")==0) { + if (seen_none) err(s->c,arm->loc,"duplicate None match arm"); + seen_none=1; + } else if (arm->text && strcmp(arm->text,"_")==0) { + wildcard=1; + } else { + err(s->c,arm->loc,"optional match arm must be Some, None, or _"); + } + if (arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); + else if (arm->a) check_expr(s,arm->a); + if (root) { + if (borrow_mut) fe_own_release_exclusive(&root->own); + else fe_own_release_shared(&root->own); + } + s->scope=old; + flow_capture(s->scope,current,count); + own_current=flow_own_new(s,count); + borrow_current=flow_borrow_new(s,count); + flow_own_capture(current,own_current,count); + flow_borrow_capture(current,borrow_current,count); + if (!have) { + for (i=0;ic,n->loc,"non-exhaustive optional match"); + if (have) m7_restore_flow(merged,own_merged,borrow_merged,count); +} + +void m7_check_match_stmt(FeCheckerState *s, FeNode *n) +{ + FeType *value; + value=check_expr(s,n->a); + if (value && value->kind==FE_TYPE_OPTIONAL) { + m7_check_optional_match(s,n,value); + n->sem_type=unknown(s->c); + return; + } + check_match(s,n); +} + +void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) +{ + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + int initialized; + expected=n->a ? node_type(s->c,n->a) : 0; + if (n->b) + stored=m7_check_expected(s,n->b,expected); + else + stored=expected ? expected : unknown(s->c); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!expected) expected=stored; + if (!n->a && n->b && fe_m7_is_null(n->b)) + err(s->c,n->loc,"null initializer requires an explicit optional type"); + if (expected && expected->kind==FE_TYPE_VOID) + err(s->c,n->loc,"variable cannot have void type"); + if (n->b && !fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) { + /* Say which rule was hit. Weakening &mut to & is a distinct thing from + two unrelated types not matching, and "type mismatch" told the reader + nothing about why the exclusive borrow could not be shared. */ + if (expected && stored && expected->kind==FE_TYPE_REF && + stored->kind==FE_TYPE_REF && !expected->ref_mut && stored->ref_mut) + err(s->c,n->loc, + "cannot rebind a mut borrow as a shared reference"); + else if (expected && stored && expected->kind==FE_TYPE_SLICE && + stored->kind==FE_TYPE_SLICE && !expected->ref_mut && + stored->ref_mut) + err(s->c,n->loc, + "cannot rebind a mut slice as a shared slice"); + else + err(s->c,n->loc,"initializer type mismatch"); + } + /* Rules the M6 declaration case carried that this one has to repeat now + that it is the only declaration case. */ + if (n->b && stored && stored->kind==FE_TYPE_VOID) + err(s->c,n->loc,"void expression cannot initialize a variable"); + if (!mutable && expected && expected->kind==FE_TYPE_SLICE && + expected->ref_mut) + err(s->c,n->loc,"let cannot bind a mutable slice"); + if (!n->b && !n->a) + err(s->c,n->loc,"uninitialized var requires an explicit type"); + if (n->b) mark_moved(s,n->b,actual); + initialized=n->b!=0; + sym=add_symbol(s,s->scope,n->text,expected,0,mutable,initialized, + local_cname(s->c,n->text ? n->text : "local"),n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth!=0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); +} + +void check_stmt(FeCheckerState *s, FeNode *n) +{ + FeScope *old; + FeNode *x; + FeType *expected; + FeType *actual; + FeType *stored; + FeSym *sym; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + old=s->scope; + s->scope=scope_new(s,old); + for (x=n->children;x;x=x->next) { + check_stmt(s,x); + own_release_after_stmt(s,s->scope,x,0); + } + own_release_after_stmt(s,s->scope,n,1); + s->scope=old; + break; + case FE_N_LET: + case FE_N_CONST: + m7_check_decl_stmt(s,n,0); + break; + case FE_N_VAR: + if (!n->a && !n->b) + err(s->c,n->loc,"uninitialized var requires an explicit type"); + m7_check_decl_stmt(s,n,1); + break; + case FE_N_ASSIGN: + expected=check_lvalue(s,n->a,compound_operator(n->text)); + stored=m7_check_expected(s,n->b,expected); + actual=n->b && n->b->sem_type ? n->b->sem_type : stored; + if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->b)) + err(s->c,n->loc,"assignment type mismatch"); + mark_moved(s,n->b,actual); + sym=n->a && n->a->kind==FE_N_IDENT ? + find_symbol(s->scope,n->a->text) : 0; + if (sym && sym->mutable) { + sym->initialized=1; + fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); + sym->moved=sym->own.move; + /* Rebinding a reference, from the M6 assignment case: the new + source has to live at least as long as the reference does, and + the previous borrow has to be released. */ + if (n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && + fe_own_is_reference_like(sym->type)) { + FeSym *root=own_root_symbol(s,n->b->a); + if (root && root->owner!=sym->owner) + err(s->c,n->b->loc,"reference would outlive its source scope"); + else if (root) { + if (sym->borrow_root) { + if (sym->borrow_mut) + fe_own_release_exclusive(&sym->borrow_root->own); + else fe_own_release_shared(&sym->borrow_root->own); + } + sym->borrow_root=root; + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + } + } + } + break; + case FE_N_EXPR_STMT: + check_expr(s,n->a); + break; + case FE_N_DEFER: + ++s->defer_depth; + check_stmt(s,n->a); + --s->defer_depth; + break; + case FE_N_IF: + if (n->text && strcmp(n->text,"comptime if")==0) { + int taken=0; + if (!comptime_condition(s,n->a,&taken)) { + err(s->c,n->a?n->a->loc:n->loc, + "comptime condition must be decidable at compile time"); + break; + } + /* SPEC 9: the branch that is not taken is parsed and nothing more. */ + if (taken) check_stmt(s,n->b); + else if (n->c) check_stmt(s,n->c); + break; + } + if (n->text && strcmp(n->text,"if let")==0) + m7_check_if_let(s,n); + else { + FeType *cond; + FeFlowSlot base[FE_M7_FLOW_CAP]; + FeFlowSlot left[FE_M7_FLOW_CAP]; + FeFlowSlot right[FE_M7_FLOW_CAP]; + FeOwnState *own_base; + FeOwnState *own_left; + FeOwnState *own_right; + FeFlowBorrow *borrow_base; + FeFlowBorrow *borrow_left; + FeFlowBorrow *borrow_right; + unsigned count; + unsigned i; + cond=check_expr(s,n->a); + if (known(cond) && cond->kind!=FE_TYPE_BOOL) + err(s->c,n->loc,"if condition must be bool"); + /* Once a function is on the M7 checker path, branch bodies must + remain on that path as well. In particular, return T inside + E!T relies on contextual success construction even when the + branch itself contains no surface M7 syntax. */ + m7_capture_flow(s,base,&own_base,&borrow_base,&count); + check_stmt(s,n->b); + flow_capture(s->scope,left,count); + own_left=flow_own_new(s,count); + borrow_left=flow_borrow_new(s,count); + flow_own_capture(left,own_left,count); + flow_borrow_capture(left,borrow_left,count); + /* A branch that always leaves contributes nothing to what follows. + Merging its state would make a value it consumed look consumed + afterwards, on a path that never ran it. */ + if (m7_stmt_definitely_exits(n->b)) { + for (i=0;ic) check_stmt(s,n->c); + if (n->c) { + flow_capture(s->scope,right,count); + own_right=flow_own_new(s,count); + borrow_right=flow_borrow_new(s,count); + flow_own_capture(right,own_right,count); + flow_borrow_capture(right,borrow_right,count); + if (m7_stmt_definitely_exits(n->c)) { + for (i=0;iret; + if (n->a) + stored=m7_check_expected(s,n->a,expected); + else + stored=fe_type_intern(&s->c->types,"void"); + actual=n->a && n->a->sem_type ? n->a->sem_type : stored; + /* R8, from the M6 return case: a returned reference has to come from a + parameter or a static, never from a local. */ + if (expected && fe_own_is_reference_like(expected) && + !own_return_from_allowed_root(s,n->a)) + err(s->c,n->loc, + "reference return must be derived from a parameter or static"); + if (n->a && stored && stored->kind==FE_TYPE_VOID && + expected && expected->kind!=FE_TYPE_VOID) + err(s->c,n->loc,"void expression returned from value function"); + if (expected && expected->kind==FE_TYPE_ERROR_UNION && n->a && + actual && actual->kind==FE_TYPE_ERROR_UNION && + !fe_type_equal(expected,actual)) + err(s->c,n->loc,"error result type mismatch"); + /* A bare `return` in a function returning `!void` is the success case: + there is no value to give, and no error either. */ + else if (!n->a && expected && expected->kind==FE_TYPE_ERROR_UNION && + expected->error_value && + expected->error_value->kind==FE_TYPE_VOID) { } + else if (!fe_type_equal(expected,stored) && + !m7_actual_compatible(expected,stored,n->a)) + err(s->c,n->loc,"return type mismatch"); + if (n->a) mark_moved(s,n->a,actual); + break; + case FE_N_WHILE: + case FE_N_FOR: + /* The core loop case carries the flow capture and merge that detects a + value moved on every iteration, and it already recurses into the body + through this function, so there is nothing to special-case here. The + M7 half used to skip all of it. */ + check_stmt_core(s,n); + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (!s->loop_depth) + err(s->c,n->loc,"break or continue outside loop"); + break; + case FE_N_UNSAFE: + check_stmt(s,n->a); + break; + default: + check_stmt_core(s,n); + break; + } +} diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c new file mode 100644 index 0000000..33f18c9 --- /dev/null +++ b/fec/src/checkexp.c @@ -0,0 +1,739 @@ +#include "checkpri.h" + +FeNode *find_const_node(FeCheck *c, const char *name) +{ + FeNode *n; + for (n=c->ast->root ? c->ast->root->children : 0; n; n=n->next) + if (n->kind==FE_N_CONST && n->text && name && strcmp(n->text,name)==0) + return n; + return 0; +} + + +const char *builtin_format(FeCheckerState *s, FeNode *fmt) +{ + FeNode *decl; + FeSym *sym; + if (fmt && fmt->kind==FE_N_LITERAL && fmt->text && fmt->text[0]=='"') + return fmt->text; + if (fmt && fmt->kind==FE_N_IDENT) { + sym=find_symbol(s->scope,fmt->text); + decl=sym && sym->decl && sym->decl->kind==FE_N_CONST ? + sym->decl : find_const_node(s->c,fmt->text); + if (decl && decl->b && decl->b->kind==FE_N_LITERAL && + decl->b->text && decl->b->text[0]=='"') { + if (!decl->a || format_is_slice_u8(fe_type_from_ast(&s->c->types,decl->a))) + return decl->b->text; + } + } + return 0; +} + +int format_is_slice_u8(FeType *t) +{ + return t && t->kind==FE_TYPE_SLICE && t->elem && + t->elem->kind==FE_TYPE_INT && strcmp(t->elem->name,"u8")==0; +} + +int format_is_writer_type(FeType *t) +{ + return t && t->kind==FE_TYPE_STRUCT && + (strcmp(t->name,"Writer")==0 || strcmp(t->name,"io.Writer")==0); +} + +int format_arg_ok(FeType *t, int verb) +{ + if (!t) return 0; + if (verb=='x') return fe_type_is_integer(t); + if (verb=='c') return t->kind==FE_TYPE_CHAR; + if (verb=='s') return format_is_slice_u8(t); + if (verb=='b') return t->kind==FE_TYPE_BOOL; + if (t->kind==FE_TYPE_INT || t->kind==FE_TYPE_BOOL || + t->kind==FE_TYPE_CHAR) return 1; + return format_is_slice_u8(t) || + (t->kind==FE_TYPE_ENUM && t->is_error); +} + +void check_format_call(FeCheckerState *s, FeNode *n) +{ + const char *fmt; + FeNode *fmt_node; + FeNode *arg; + FeNode *x; + FeType *t; + unsigned long i,j; + unsigned count=0; + unsigned argc=0; + unsigned offset=0; + int verb; + int bad=0; + int counted=0; + if (strcmp(n->text,"@fprint")==0) offset=1; + fmt_node=n->children; + if (offset) { + if (!fmt_node) { err(s->c,n->loc,"@fprint requires a writer"); return; } + t=check_expr(s,fmt_node); + if (!format_is_writer_type(t)) + err(s->c,fmt_node->loc,"@fprint requires io.Writer"); + fmt_node=fmt_node->next; + } + if (strcmp(n->text,"@sprint")==0) { + if (!fmt_node) { err(s->c,n->loc,"@sprint requires a buffer"); return; } + t=check_expr(s,fmt_node); + if (!format_is_slice_u8(t) || !t->ref_mut) + err(s->c,fmt_node->loc,"@sprint requires []mut u8 buffer"); + fmt_node=fmt_node->next; + } + fmt=builtin_format(s,fmt_node); + if (!fmt) { err(s->c,n->loc,"format must be a comptime string"); return; } + n->aux_text=(char *)fmt; + arg=fmt_node ? fmt_node->next : 0; + for (x=arg;x;x=x->next) { check_expr(s,x); ++argc; } + i=1; + while (fmt[i] && fmt[i]!='"') { + if (fmt[i]=='\\') { if (fmt[i+1]) ++i; ++i; continue; } + if (fmt[i]=='{' && fmt[i+1]=='{') { i+=2; continue; } + if (fmt[i]=='}' && fmt[i+1]=='}') { i+=2; continue; } + if (fmt[i]=='{') { + j=i+1; + while (fmt[j] && fmt[j]!='}') ++j; + if (!fmt[j]) { err(s->c,n->loc,"unterminated format placeholder"); bad=1; break; } + if (j==i+1) verb=' '; else if (j==i+2) verb=(unsigned char)fmt[i+1]; else verb='?'; + if (verb!=' ' && verb!='x' && verb!='c' && verb!='s' && verb!='b') { + err(s->c,n->loc,"unsupported format verb"); bad=1; + } + if (!arg) { + err(s->c,n->loc,"format argument count mismatch"); + bad=1; counted=1; + } + else { + t=arg->sem_type; + if (verb==' ' && t && t->kind==FE_TYPE_ENUM && t->is_error) verb='s'; + if (!format_arg_ok(t,verb)) { err(s->c,arg->loc,"no fmt writer for argument type"); bad=1; } + arg=arg->next; + } + ++count; i=j+1; continue; + } + if (fmt[i]=='}') { err(s->c,n->loc,"unmatched '}' in format"); bad=1; } + ++i; + } + /* Running out of arguments mid-string already said this. Saying it again + once the whole string has been walked adds nothing. */ + if (count!=argc && !counted) { err(s->c,n->loc,"format argument count mismatch"); bad=1; } + (void)bad; +} + +int is_format_builtin(const char *name) +{ + return name && (strcmp(name,"@print")==0 || strcmp(name,"@fprint")==0 || + strcmp(name,"@sprint")==0); +} + +int lvalue_writable(FeCheckerState *s, FeNode *n) +{ + FeSym *sym; + FeType *t; + if (!n) return 0; + if (n->kind == FE_N_IDENT) { + sym=find_symbol(s->scope,n->text ? n->text : ""); + return sym ? sym->mutable : 0; + } + if (n->kind == FE_N_MEMBER) { + t=n->a ? n->a->sem_type : 0; + if (t && t->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) return t->ref_mut; + return lvalue_writable(s,n->a); + } + if (n->kind == FE_N_INDEX) return lvalue_writable(s,n->a); + return 0; +} + +int has_field(FeNode *list, const char *name) +{ + FeNode *f; + for (f=list; f; f=f->next) + if (f->text && name && strcmp(f->text,name)==0) return 1; + return 0; +} + +/* A field of a type declared elsewhere is reachable only with `pub`. Inside + the declaring unit every field is reachable, `pub` or not. */ +int field_is_visible(FeCheckerState *s, const FeType *t, + const FeFieldType *field) +{ + if (!t || !t->unit) return 1; + if (s->c->types.unit_name && + strcmp(t->unit,s->c->types.unit_name)==0) return 1; + return field && field->ast_node && + (field->ast_node->flags & FE_NODE_PUB)!=0; +} + +/* The field list of a struct literal, once the type is known. Reached from + both `Type{...}` and `binding.Type{...}`. */ +FeType *check_struct_fields(FeCheckerState *s, FeNode *n, FeType *t) +{ + FeFieldType *field; + FeNode *f; + FeType *v; + unsigned i; + for(f=n->children;f;f=f->next) if(f->kind==FE_N_FIELD) { + if(has_field(f->next,f->text)) { err(s->c,f->loc,"duplicate struct field"); } + field=fe_type_field(t,f->text); + if(!field) { err(s->c,f->loc,"invalid struct field"); continue; } + if(!field_is_visible(s,t,field)) { + err(s->c,f->loc,"field is private to its unit"); + continue; + } + v=check_expr(s,f->a); + mark_moved(s,f->a,v); + if(!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"struct field type mismatch"); + } + for(i=0;ifield_count;i++) if(!has_field(n->children,t->fields[i].name)) err(s->c,n->loc,"missing struct field"); + n->sem_type=t; return t; +} + +FeType *check_struct_init(FeCheckerState *s, FeNode *n) +{ + FeType *t; + FeFieldType *field; + FeNode *f; + FeType *v; + FeType *et; + FeVariantType *variant; + if (n->a && n->a->kind == FE_N_MEMBER) { + FeUnit *home=binding_unit(s,n->a->a); + if (home) { + /* `binding.Type{...}` names a type in another unit. */ + const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; + FeNode *decl=unit_type_decl(s->c,home,want); + t=unit_type(s->c,home,want); + if (!t || !decl) { err(s->c,n->a->loc,"unknown name"); return unknown(s->c); } + if (!decl_is_public(decl)) { + err(s->c,n->a->loc,"type is private to its unit"); + return unknown(s->c); + } + if (t->kind!=FE_TYPE_STRUCT) { + err(s->c,n->loc,"unknown struct type"); + return unknown(s->c); + } + return check_struct_fields(s,n,t); + } + et=check_expr(s,n->a->a); + variant=et && et->kind==FE_TYPE_ENUM ? + fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; + if (!variant) { err(s->c,n->loc,"invalid enum variant"); return unknown(s->c); } + if (variant->field_count != 0) { + for (f=n->children; f; f=f->next) { + if (f->kind != FE_N_FIELD) continue; + field=0; + if (variant->fields) { + unsigned i; + for(i=0;ifield_count;i++) if(strcmp(variant->fields[i].name,f->text)==0) field=&variant->fields[i]; + } + if (!field) { err(s->c,f->loc,"invalid enum payload field"); continue; } + v=check_expr(s,f->a); + mark_moved(s,f->a,v); + if (!compatible(field->type,v,f->a) && v->kind!=FE_TYPE_UNKNOWN) err(s->c,f->loc,"enum payload type mismatch"); + } + } else if (n->children) err(s->c,n->loc,"empty enum variant cannot have payload"); + n->sem_type=et; return et; + } + t=fe_type_intern(&s->c->types,n->text ? n->text : ""); + if (!t || t->kind!=FE_TYPE_STRUCT) { err(s->c,n->loc,"unknown struct type"); return unknown(s->c); } + return check_struct_fields(s,n,t); +} + +FeType *check_array_init(FeCheckerState *s, FeNode *n) +{ + FeNode *x; FeType *elem=0; FeType *v; unsigned long count=0; + for(x=n->children;x;x=x->next) { v=check_expr(s,x); mark_moved(s,x,v); if(!elem) elem=v; else if(!compatible(elem,v,x)&&v->kind!=FE_TYPE_UNKNOWN) err(s->c,x->loc,"array element type mismatch"); ++count; } + if(!elem) elem=unknown(s->c); + n->sem_type=fe_type_array(&s->c->types,count,elem); return n->sem_type; +} + +int array_slice_lvalue(FeNode *n) +{ + return n && (n->kind==FE_N_IDENT || n->kind==FE_N_MEMBER || + n->kind==FE_N_INDEX); +} + +FeType *check_index(FeCheckerState *s, FeNode *n) +{ + FeType *base=check_expr(s,n->a); FeType *idx; FeType *elem; + if(!fe_type_is_indexable(base)) { err(s->c,n->loc,"indexing requires an array or slice"); return unknown(s->c); } + if(n->b) { idx=check_expr(s,n->b); if(known(idx)&&!fe_type_is_integer(idx)) err(s->c,n->loc,"index must be an integer"); } + if(n->c || !n->b) { + if (base->kind==FE_TYPE_ARRAY && !array_slice_lvalue(n->a)) + err(s->c,n->loc,"array slicing requires a stable lvalue"); + if(n->c) { + idx=check_expr(s,n->c); + if(known(idx)&&!fe_type_is_integer(idx)) + err(s->c,n->loc,"slice bound must be an integer"); + } + elem=base->elem; + n->sem_type=(base->kind==FE_TYPE_SLICE ? base->ref_mut : + lvalue_writable(s,n->a)) ? + fe_type_mut_slice(&s->c->types,elem) : + fe_type_slice(&s->c->types,elem); + return n->sem_type; + } + n->sem_type=base->elem; return n->sem_type; +} + +FeType *check_identifier(FeCheckerState *s, FeNode *n) +{ + FeSym *sym; + sym = find_symbol(s->scope, n->text ? n->text : ""); + if (!sym) { + FeType *named=fe_type_intern(&s->c->types,n->text ? n->text : ""); + if(named->kind==FE_TYPE_STRUCT || named->kind==FE_TYPE_ENUM) { n->sem_type=named; return named; } + if(named->kind!=FE_TYPE_UNKNOWN) { + err(s->c, n->loc, "a type is not a value here"); + return unknown(s->c); + } + err(s->c, n->loc, "unknown name"); + return unknown(s->c); + } + n->cname = sym->cname; + n->sem_type = sym->type; + if (!sym->fn) { + fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); + sym->moved=sym->own.move; + } + return sym->type; +} + +FeType *check_expr_core(FeCheckerState *s, FeNode *n) +{ + FeCheck *c = s->c; + FeType *a; + FeType *b; + FeSym *sym; + FeNode *x; + FeNode *param; + FeNode *arg; + FeType *et; + FeFieldType *field; + FeVariantType *variant; + const char *op; + if (!n) return unknown(c); + if (n->kind == FE_N_IDENT) + return check_identifier(s, n); + if (n->kind == FE_N_LITERAL) { + if (!n->text) return unknown(c); + if (strcmp(n->text, "true") == 0 || strcmp(n->text, "false") == 0) + a = fe_type_intern(&c->types, "bool"); + else if (n->text[0] == '\'') + a = fe_type_intern(&c->types, "char"); + else if (n->text[0] == '"') + a = fe_type_intern(&c->types, "str"); + else + a = fe_type_intern(&c->types, "i32"); + n->sem_type = a; + return a; + } + if (n->kind == FE_N_STRUCT_INIT) return check_struct_init(s,n); + if (n->kind == FE_N_ARRAY_INIT) return check_array_init(s,n); + if (n->kind == FE_N_INDEX) return check_index(s,n); + if (n->kind == FE_N_MATCH) { check_match(s,n); n->sem_type=unknown(c); return n->sem_type; } + if (n->kind == FE_N_UNARY) { + a = check_expr(s, n->a); + op = n->text ? n->text : ""; + if (strcmp(op, "not") == 0) { + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "'not' requires bool"); + a = fe_type_intern(&c->types, "bool"); + } else if (strcmp(op, "-") == 0) { + if (known(a) && !fe_type_is_integer(a)) + err(c, n->loc, "unary '-' requires integer"); + } else if (strcmp(op, "try") == 0) { + /* SPEC 6.4: try is only allowed inside a function returning an error + union. Checked on the expression rather than on the statement so + that it also covers `var x = try e;` and `x = try e;`, which the + statement-level check walked straight past. */ + if (!s->ret || s->ret->kind != FE_TYPE_ERROR_UNION) + err(c,n->loc,"try requires an enclosing error result"); + if (a && a->kind==FE_TYPE_ERROR_UNION) + a=a->error_value; + else { + err(c,n->loc,"try requires an error result"); + a=unknown(c); + } + } else if (strcmp(op,"&")==0 || strcmp(op,"&mut")==0) { + if (strcmp(op,"&mut")==0 && a && a->kind==FE_TYPE_REF && !a->ref_mut) + err(c,n->loc,"cannot create mutable borrow from a shared reference"); + own_borrow_expr(s,n->a,strcmp(op,"&mut")==0); + a=fe_type_ref(&c->types,a,strcmp(op,"&mut")==0); + } + n->sem_type = a; + return a; + } + if (n->kind == FE_N_TYPE && n->text && strcmp(n->text, "as") == 0) { + a = check_expr(s, n->a); + b = node_type(c, n->b); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "cast target cannot be void"); + else if (known(a) && known(b) && !explicit_castable(a,b)) + err(c, n->loc, "'as' requires integer or char types"); + n->sem_type = b; + return b; + } + if (n->kind == FE_N_BINARY) { + a = check_expr(s, n->a); + b = check_expr(s, n->b); + op = n->text ? n->text : ""; + if (strcmp(op, "and") == 0 || strcmp(op, "or") == 0) { + if ((known(a) && a->kind != FE_TYPE_BOOL) || + (known(b) && b->kind != FE_TYPE_BOOL)) + err(c, n->loc, "logical operator requires bool operands"); + a = fe_type_intern(&c->types, "bool"); + } else if (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 || + strcmp(op, "<") == 0 || strcmp(op, "<=") == 0 || + strcmp(op, ">") == 0 || strcmp(op, ">=") == 0) { + if (known(a) && known(b) && !fe_type_equal(a, b) && + !compatible(a, b, n->b) && !compatible(b, a, n->a)) + err(c, n->loc, "comparison operands have different types"); + else if (strcmp(op,"==")!=0 && strcmp(op,"!=")!=0 && + ((known(a) && !ordered_type(a)) || + (known(b) && !ordered_type(b)))) + err(c, n->loc, "ordering requires integer or char operands"); + a = fe_type_intern(&c->types, "bool"); + } else { + if ((known(a) && !fe_type_is_integer(a)) || + (known(b) && !fe_type_is_integer(b)) || + (known(a) && known(b) && !fe_type_equal(a, b) && + !compatible(a, b, n->b) && !compatible(b, a, n->a))) + err(c, n->loc, + "arithmetic operands must have the same integer type"); + } + n->sem_type = a; + return a; + } + if (n->kind == FE_N_CALL) { + if (n->a && n->a->kind==FE_N_MEMBER && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"drop")==0) { + err(c,n->loc,"drop may only be invoked by scope cleanup"); + return unknown(c); + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"mem")==0 && n->a->b && n->a->b->text) { + FeNode *arg=n->children; + if (strcmp(n->a->b->text,"destroy")==0) { + a=arg ? check_expr(s,arg) : unknown(c); + if (!arg || arg->next || !a || a->kind!=FE_TYPE_OWNED) + err(c,n->loc,"mem.destroy requires exactly one owned pointer"); + else + mark_moved(s,arg,a); + n->sem_type=fe_type_intern(&c->types,"void"); + return n->sem_type; + } + if (strcmp(n->a->b->text,"create")==0) { + if (!arg || arg->next) + err(c,n->loc,"mem.create requires exactly one value"); + a=arg ? check_expr(s,arg) : unknown(c); + if(arg) mark_moved(s,arg,a); + a=fe_type_owned(&c->types,a); + n->sem_type=fe_type_error_union(&c->types,a); + return n->sem_type; + } + if (strcmp(n->a->b->text,"alloc_slice")==0) { + FeNode *count=arg ? arg->next : 0; + FeType *item; + if(!arg || arg->kind!=FE_N_IDENT || !count || count->next) + err(c,n->loc,"mem.alloc_slice requires a type and length"); + item=arg && arg->kind==FE_N_IDENT ? + fe_type_intern(&c->types,arg->text) : unknown(c); + b=count ? check_expr(s,count) : unknown(c); + if(known(b) && !fe_type_is_integer(b)) + err(c,count->loc,"slice length must be an integer"); + /* Freshly allocated storage is owned outright, so it is + writable: there is nobody else to disturb. */ + a=fe_type_owned(&c->types,fe_type_mut_slice(&c->types,item)); + n->sem_type=fe_type_error_union(&c->types,a); + return n->sem_type; + } + if (strcmp(n->a->b->text,"replace")==0) { + FeNode *value=arg ? arg->next : 0; + if(!arg || !value || value->next) + err(c,n->loc,"mem.replace requires destination and value"); + a=arg ? check_expr(s,arg) : unknown(c); + if(!a || a->kind!=FE_TYPE_REF || !a->ref_mut || + !arg->a || !lvalue_writable(s,arg->a)) + err(c,n->loc,"mem.replace destination must be a mutable place"); + b=value ? check_expr(s,value) : unknown(c); + if(a && a->kind==FE_TYPE_REF && !compatible(a->elem,b,value)) + err(c,value->loc,"mem.replace value type mismatch"); + if(value) mark_moved(s,value,b); + n->sem_type=a && a->kind==FE_TYPE_REF ? a->elem : unknown(c); + fe_type_require_replace(&c->types,n->sem_type); + return n->sem_type; + } + } + if (n->a && n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->a->text && + strcmp(n->a->a->text,"io")==0 && n->a->b && n->a->b->text && + strcmp(n->a->b->text,"null_writer")==0) { + FeNode *arg=n->children; + if (arg) err(c,n->loc,"io.null_writer takes no arguments"); + n->sem_type=fe_type_intern(&c->types,"io.Writer"); + return n->sem_type; + } + if (n->text && is_format_builtin(n->text)) { + check_format_call(s,n); + if (strcmp(n->text,"@print")==0) + n->sem_type=fe_type_intern(&c->types,"void"); + else if (strcmp(n->text,"@sprint")==0) + n->sem_type=fe_type_intern(&c->types,"usize"); + else + n->sem_type=fe_type_error_union(&c->types,fe_type_intern(&c->types,"void")); + return n->sem_type; + } + if (!n->a && n->text && (strcmp(n->text,"@size_of")==0 || strcmp(n->text,"@align_of")==0)) { + FeNode *type_arg=n->children; + FeType *target=type_arg && type_arg->kind==FE_N_IDENT ? fe_type_intern(&c->types,type_arg->text) : unknown(c); + if(!target || !known(target)) err(c,n->loc,"size/align requires a known type"); + n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; + } + if (n->a && n->a->kind == FE_N_MEMBER) { + FeNode *method; + FeNode *self_param; + FeUnit *home=binding_unit(s,n->a->a); + if (home) { + const char *want=n->a->b && n->a->b->text ? n->a->b->text : ""; + FeSym *fsym=unit_member(c,home,want); + if (!fsym) { + err(c,n->a->loc,"unknown name"); + for (x=n->children;x;x=x->next) check_expr(s,x); + return unknown(c); + } + if (!decl_is_public(fsym->decl)) { + err(c,n->a->loc,"name is private to its unit"); + for (x=n->children;x;x=x->next) check_expr(s,x); + return unknown(c); + } + if (decl_is_generic(fsym->fn)) + return check_generic_call(s,n,fsym,home); + return check_call_args(s,n,fsym,home->name,0); + } + { + int names_type=0; + FeType *owner_type=type_from_expr(s,n->a->a,&names_type); + if (names_type && owner_type && + owner_type->kind==FE_TYPE_STRUCT) { + FeNode *m=type_method(owner_type, + n->a->b ? n->a->b->text : ""); + if (!m) { err(c,n->a->loc,"unknown method"); return unknown(c); } + if (!method_is_static(m)) { + err(c,n->loc,"method requires a receiver"); + return unknown(c); + } + return check_static_method_call(s,n,owner_type,m); + } + } + et=check_expr(s,n->a->a); + /* A method can be reached through a reference or an owner as well + as through the value itself. */ + if (et && (et->kind==FE_TYPE_REF || et->kind==FE_TYPE_OWNED) && + et->elem && et->elem->kind==FE_TYPE_STRUCT && + find_method(c,et->elem,n->a->b ? n->a->b->text : "")) + et=et->elem; + method=et && et->kind==FE_TYPE_STRUCT ? + find_method(c,et,n->a->b ? n->a->b->text : "") : 0; + if(method) { + FeBindSave msave; + int bound=0; + self_param=method->a ? method->a->children : 0; + if(!self_param) { + err(c,n->loc,"method requires self parameter"); + return unknown(c); + } + /* A method of a generic instance reads its signature with that + instance's arguments bound. */ + if (et->bind_count) { + push_instance_bindings(c,&msave,et); + bind_self(c,et); + bound=1; + } + a=method_type(c,self_param->a,et); + if(a->kind==FE_TYPE_REF && a->ref_mut && + !lvalue_writable(s,n->a->a)) + err(c,n->loc,"mutable method requires a mutable receiver"); + if(a->kind!=FE_TYPE_REF) mark_moved(s,n->a->a,et); + param=self_param->next; + arg=n->children; + while(param && arg) { + a=check_expr(s,arg); + b=method_type(c,param->a,et); + if(!compatible(b,a,arg) && a->kind!=FE_TYPE_UNKNOWN) + err(c,arg->loc,"method argument type mismatch"); + mark_moved(s,arg,a); + param=param->next; + arg=arg->next; + } + if(param || arg) err(c,n->loc,"wrong number of method arguments"); + n->sem_decl=method; + n->sem_type=method->b ? method_type(c,method->b,et) : + fe_type_intern(&c->types,"void"); + if (bound) { + pop_bindings(c,&msave); + check_instance_method(s,et,method,n->loc,n); + } + return n->sem_type; + } + if (et && (et->kind==FE_TYPE_SLICE || et->kind==FE_TYPE_STR) && + n->a->b && n->a->b->text && + strcmp(n->a->b->text,"trim")==0) { + if (n->children) err(c,n->loc,"trim takes no arguments"); + n->sem_type=fe_type_slice(&c->types,et->elem); + return n->sem_type; + } + variant=et && et->kind==FE_TYPE_ENUM ? + fe_type_variant(et,n->a->b ? n->a->b->text : "") : 0; + arg=n->children; + if (!variant) { err(c,n->loc,"invalid enum variant constructor"); return unknown(c); } + if (variant->field_count==1 && arg) { + FeType *av=check_expr(s,arg); + if(!compatible(variant->fields[0].type,av,arg)&&av->kind!=FE_TYPE_UNKNOWN) err(c,arg->loc,"enum payload type mismatch"); + } else if (variant->field_count != 0 || arg) err(c,n->loc,"wrong enum payload arity"); + n->sem_type=et; return et; + } + if (n->a && n->a->kind == FE_N_IDENT) { + sym = find_symbol(s->scope, n->a->text ? n->a->text : ""); + if (!sym) { + err(c, n->loc, "unknown function"); + return unknown(c); + } + if (decl_is_generic(sym->fn)) + return check_generic_call(s,n,sym,current_unit(c)); + return check_call_args(s, n, sym, 0, 0); + } + for (x = n->children; x; x = x->next) check_expr(s, x); + return unknown(c); + } + if (n->kind == FE_N_MEMBER) { + if (is_error_set_member(s,n)) { + n->sem_type=fe_type_intern(&c->types,"core.Error"); + return n->sem_type; + } + if (n->a && n->a->kind==FE_N_IDENT && n->a->text && + strcmp(n->a->text,"io")==0 && n->b && n->b->text && + (strcmp(n->b->text,"stdout")==0 || + strcmp(n->b->text,"stderr")==0)) { + n->sem_type=fe_type_intern(&c->types,"io.Writer"); + return n->sem_type; + } + a=check_expr(s,n->a); + if (a->kind == FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=a->elem; + return a->elem; + } + if(a->kind==FE_TYPE_REF && a->elem && + a->elem->kind==FE_TYPE_STRUCT) { + field=fe_type_field(a->elem,n->b ? n->b->text : ""); + if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } + n->sem_type=field->type; + return field->type; + } + if (a->kind == FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=a->elem; + return a->elem; + } + if(a->kind==FE_TYPE_STRUCT) { + field=fe_type_field(a,n->b ? n->b->text : ""); + if(!field) { err(c,n->loc,"unknown struct field"); return unknown(c); } + n->sem_type=field->type; return field->type; + } + if(a->kind==FE_TYPE_ENUM) { + if(!fe_type_variant(a,n->b ? n->b->text : "")) err(c,n->loc,"unknown enum variant"); + n->sem_type=a; return a; + } + if((a->kind==FE_TYPE_SLICE || a->kind==FE_TYPE_STR) && n->b && + strcmp(n->b->text,"n")==0) { + n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; + } + return unknown(c); + } + return unknown(c); +} + +/* `base_in` is the already-checked type of a member expression's base. The M7 + lvalue path looks at that base before delegating here, and checking it a + second time reports any ownership violation on it a second time too. */ +FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, + FeType *base_in) +{ + FeSym *sym; + FeType *base; + FeFieldType *field; + if (n && n->kind == FE_N_IDENT) { + sym = find_symbol(s->scope, n->text ? n->text : ""); + if (!sym) { + err(s->c, n->loc, "unknown name"); + return unknown(s->c); + } + if (sym->fn) { + err(s->c, n->loc, "function is not assignable"); + return unknown(s->c); + } + if (!sym->mutable) + err(s->c, n->loc, "cannot assign to immutable let"); + n->cname = sym->cname; + n->sem_type = sym->type; + if (read) { + fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); + sym->moved=sym->own.move; + } + return sym->type; + } + if (n && n->kind == FE_N_MEMBER) { + base=base_in ? base_in : check_expr(s,n->a); + if (base && base->kind == FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + if (!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + n->sem_type=base->elem; + return base->elem; + } + if(base && base->kind==FE_TYPE_REF && base->elem && + base->elem->kind==FE_TYPE_STRUCT) { + if(!base->ref_mut) + err(s->c,n->loc,"cannot write through shared reference"); + field=fe_type_field(base->elem,n->b ? n->b->text : ""); + if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } + n->sem_type=field->type; + return field->type; + } + if (base && base->kind == FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return base->elem; + } + if (!lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + field=base && base->kind==FE_TYPE_STRUCT ? fe_type_field(base,n->b ? n->b->text : "") : 0; + if(!field) { err(s->c,n->loc,"assignment requires a valid struct field"); return unknown(s->c); } + n->sem_type=field->type; return field->type; + } + if (n && n->kind == FE_N_INDEX) { + base=check_index(s,n); + if (n->a && n->a->sem_type && + n->a->sem_type->kind == FE_TYPE_SLICE && + !n->a->sem_type->ref_mut) + err(s->c,n->loc,"cannot write through shared slice"); + else if (n->a && n->a->sem_type && + n->a->sem_type->kind != FE_TYPE_SLICE && + !lvalue_writable(s,n->a)) + err(s->c,n->loc,"cannot assign through immutable value"); + return base; + } + if (n) err(s->c, n->loc, "assignment requires a variable"); + return unknown(s->c); +} + +int compound_operator(const char *op) +{ + return op && strcmp(op, "=") != 0; +} diff --git a/fec/src/checkgen.c b/fec/src/checkgen.c new file mode 100644 index 0000000..9be5059 --- /dev/null +++ b/fec/src/checkgen.c @@ -0,0 +1,618 @@ +#include "checkpri.h" + +unsigned decl_type_param_count(const FeNode *decl) +{ + FeNode *p; + unsigned n=0; + if (!decl) return 0; + if (decl->kind==FE_N_FN) { + for (p=decl->a?decl->a->children:0;p;p=p->next) + if (p->flags & FE_NODE_COMPTIME) ++n; + return n; + } + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) + for (p=decl->a?decl->a->children:0;p;p=p->next) ++n; + return n; +} + +FeNode *decl_type_param(const FeNode *decl, unsigned i) +{ + FeNode *p; + unsigned n=0; + if (!decl) return 0; + if (decl->kind==FE_N_FN) { + for (p=decl->a?decl->a->children:0;p;p=p->next) + if (p->flags & FE_NODE_COMPTIME) { if (n==i) return p; ++n; } + return 0; + } + for (p=decl->a?decl->a->children:0;p;p=p->next) { if (n==i) return p; ++n; } + return 0; +} + +int decl_is_generic(const FeNode *decl) +{ + return decl_type_param_count(decl)!=0; +} + +/* SPEC 9: v0.1 has comptime type parameters and no other kind. */ +void check_generic_params(FeCheck *c, FeNode *decl) +{ + FeNode *p; + if (!decl || decl->kind!=FE_N_FN) return; + for (p=decl->a?decl->a->children:0;p;p=p->next) { + if (!(p->flags & FE_NODE_COMPTIME)) continue; + if (!p->a || p->a->kind!=FE_N_TYPE || !p->a->text || + strcmp(p->a->text,"type")!=0) + err(c,p->loc,"a comptime parameter must be a type parameter"); + } +} + +void push_bindings(FeCheck *c, FeBindSave *save, FeNode *decl, + FeType **args, unsigned count) +{ + unsigned i; + save->count=c->types.param_count; + for (i=0;iparams[i]=c->types.params[i]; + c->types.param_count=0; + for (i=0;itypes.params[i].name=p && p->text ? p->text : "?"; + c->types.params[i].type=args[i]; + ++c->types.param_count; + } +} + +/* Restore the bindings recorded on an instance, so a method sees exactly the + environment its type was built with. */ +void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t) +{ + unsigned i; + save->count=c->types.param_count; + for (i=0;iparams[i]=c->types.params[i]; + c->types.param_count=0; + for (i=0;ibind_count && itypes.params[c->types.param_count++]=t->binds[i]; +} + +void bind_self(FeCheck *c, FeType *owner) +{ + if (c->types.param_count>=FE_TYPE_PARAM_MAX) return; + c->types.params[c->types.param_count].name="Self"; + c->types.params[c->types.param_count].type=owner; + ++c->types.param_count; +} + +void pop_bindings(FeCheck *c, const FeBindSave *save) +{ + unsigned i; + for (i=0;itypes.params[i]=save->params[i]; + c->types.param_count=save->count; +} + +/* `unit.Name(arg,arg)` -- the canonical identity of one instance. + Nesting makes the readable spelling grow without bound, and a spelling that + got cut off would make two different instances look like the same one, so + past a length the arguments are written as serial numbers instead. Those are + unique, so identity stays exact even where the spelling stops being + readable. */ + +void instance_key(char *out, const char *unit, const char *name, + FeType **args, unsigned count) +{ + unsigned i; + unsigned long n=0; + unsigned long cap=(unsigned long)FE_GENERIC_NAME_READABLE; + const char *p; + char number[24]; + int readable=1; + for (p=unit?unit:"";*p;++p) { if (nname[0] ? args[i]->name : "?";*p;++p) { + if (nserial : 0U); + for (p=number;*p && ninstance_count;++i) + if (!strcmp(c->instances[i].key,key)) return c->instances[i].cname; + return 0; +} + +int instance_known(FeCheck *c, const char *key) +{ + unsigned i; + for (i=0;iinstance_count;++i) + if (strcmp(c->instances[i].key,key)==0) return 1; + return 0; +} + +int instance_record(FeCheck *c, const char *key, FeLoc loc, + FeNode *decl, FeUnit *home, FeType *owner) +{ + FeInstance *inst; + unsigned i; + if (instance_known(c,key)) return 0; + if (c->instance_count>=FE_GENERIC_INSTANCE_MAX) { + err(c,loc,"too many generic instances"); + return -1; + } + inst=&c->instances[c->instance_count]; + strcpy(inst->key,key); + inst->decl=decl; + inst->home=home ? home->name : 0; + inst->owner=owner; + inst->cname=unit_cname(c,key); + /* The bindings in force right now are the ones this instance was built + with, and lowering has to see exactly those again. */ + inst->bind_count=c->types.param_count; + for (i=0;itypes.param_count && ibinds[i]=c->types.params[i]; + ++c->instance_count; + return 1; +} + +/* One step further down a chain of instantiations. Chains that keep producing + new instances are the ones that never end, so the limit counts nesting. */ +int instance_descend(FeCheck *c, FeLoc loc) +{ + if (c->instance_depth>=FE_GENERIC_DEPTH_MAX) { + err(c,loc,"generic instantiation depth exceeded"); + return 0; + } + ++c->instance_depth; + return 1; +} + +FeUnit *current_unit(FeCheck *c) +{ + unsigned u; + for (u=0;ubuild->count;++u) + if (strcmp(c->build->units[u].name,c->types.unit_name)==0) + return &c->build->units[u]; + return c->unit; +} + +/* Build `Box(i32)`: the declaration's fields with the parameters bound, under + a name that records which arguments made it. */ +FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, + const char *key, FeType **args, + unsigned count) +{ + FeBindSave save; + FeType *t; + FeNode *f; + unsigned fields=0; + unsigned i=0; + t=fe_type_intern_unit(&c->types,home->name,key); + if (!t || t->kind!=FE_TYPE_UNKNOWN) return t; + t->kind=FE_TYPE_STRUCT; + t->packed=(decl->flags & FE_NODE_PACKED)!=0; + t->decl_node=decl; + t->bind_count=0; + for (i=0;ibinds[t->bind_count].name=p && p->text ? p->text : "?"; + t->binds[t->bind_count].type=args[i]; + ++t->bind_count; + } + t->cname=unit_cname(c,key); + for (f=decl->children;f;f=f->next) + if (f->kind==FE_N_FN && f->text && strcmp(f->text,"drop")==0) + t->has_drop=1; + for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) ++fields; + t->field_count=fields; + if (fields) { + t->fields=(FeFieldType *)fe_arena_alloc(&c->arena, + fields*sizeof(FeFieldType)); + if (!t->fields) { t->field_count=0; return t; } + push_instance_bindings(c,&save,t); + bind_self(c,t); + i=0; + for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) { + t->fields[i].name=f->text; + t->fields[i].type=node_type(c,f->a); + t->fields[i].offset=0; + t->fields[i].ast_node=f; + ++i; + } + pop_bindings(c,&save); + } + fe_type_layout_all(&c->types); + /* A type that says how to let go of itself needs that method to exist for + every instance, whether or not anyone calls it by name: scope cleanup + will. */ + { + FeNode *release; + for (release=decl->children;release;release=release->next) + if (release->kind==FE_N_FN && release->text && + !strcmp(release->text,"drop") && release->c) { + FeCheckerState s; + memset(&s,0,sizeof s); + s.c=c; + s.scope=c->unit_scope[unit_index(c,home)]; + s.globals=s.scope; + check_instance_method(&s,t,release,decl->loc,0); + break; + } + } + return t; +} + +FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, + FeType **args, unsigned count, FeLoc loc) +{ + FeNode *decl=unit_type_decl(c,home,name); + char key[FE_GENERIC_KEY_MAX]; + if (!decl || !decl_is_generic(decl)) { + err(c,loc,"type does not take generic arguments"); + return unknown(c); + } + if (decl->kind!=FE_N_STRUCT) { + err(c,loc,"only a generic struct can be instantiated"); + return unknown(c); + } + if (count!=decl_type_param_count(decl)) { + err(c,loc,"wrong number of generic arguments"); + return unknown(c); + } + instance_key(key,home->name,name,args,count); + if (instance_record(c,key,loc,decl,home,0)<0) return unknown(c); + return build_struct_instance(c,home,decl,key,args,count); +} + +/* `Name(args...)` written in type position. */ +FeType *instantiate_type_node(void *owner, const FeNode *node) +{ + FeCheck *c=(FeCheck *)owner; + FeUnit *home=current_unit(c); + const char *name=node->text; + FeNode *arg; + FeType *args[FE_TYPE_PARAM_MAX]; + unsigned count=0; + FeType *result; + /* `binding.Name` names a type in another unit. The binding is not itself a + type, so it has to be peeled off before anything is looked up. */ + if (node->a && node->a->kind==FE_N_IDENT && node->a->text && c->build && + c->unit) { + FeUnit *bound=fe_build_binding(c->build,c->unit,node->text); + if (bound) { home=bound; name=node->a->text; } + } + if (!node->children) { + FeNode *decl=unit_type_decl(c,home,name ? name : ""); + if (decl && decl_is_generic(decl)) { + /* A generic declaration is not a type until it has arguments. */ + err(c,node->loc,"generic type requires type arguments"); + return unknown(c); + } + if (name!=node->text) { + FeType *there=unit_type(c,home,name); + if (there) return there; + } + return fe_type_intern(&c->types,name); + } + if (!instance_descend(c,node->loc)) return unknown(c); + for (arg=node->children;arg;arg=arg->next) { + if (counttypes,arg); + ++count; + } + if (count>FE_TYPE_PARAM_MAX) { + err(c,node->loc,"wrong number of generic arguments"); + --c->instance_depth; + return unknown(c); + } + result=instantiate_struct(c,home,name ? name : "",args,count, + node->loc); + --c->instance_depth; + return result; +} + +/* A type written where an expression is: `i32`, `Box(i32)`. Only a comptime + argument position accepts one. */ +FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) +{ + FeCheck *c=s->c; + FeType *t; + unsigned i; + *ok=0; + if (!n) return unknown(c); + if (n->kind==FE_N_IDENT && n->text) { + for (i=0;itypes.param_count;++i) + if (strcmp(c->types.params[i].name,n->text)==0) { + *ok=1; + return c->types.params[i].type; + } + if (find_symbol(s->scope,n->text)) { + /* A const alias of a type is that type (SPEC 4.7). */ + FeSym *sym=find_symbol(s->scope,n->text); + if (sym && sym->decl && sym->decl->kind==FE_N_CONST && + sym->decl->b && sym->decl->b->kind==FE_N_IDENT) + return type_from_expr(s,sym->decl->b,ok); + return unknown(c); + } + t=fe_type_intern(&c->types,n->text); + if (t && t->kind!=FE_TYPE_UNKNOWN) { *ok=1; return t; } + return unknown(c); + } + if (n->kind==FE_N_CALL && n->a && + (n->a->kind==FE_N_IDENT || + (n->a->kind==FE_N_MEMBER && n->a->a && + n->a->a->kind==FE_N_IDENT && n->a->b && n->a->b->text))) { + FeType *args[FE_TYPE_PARAM_MAX]; + unsigned count=0; + FeNode *arg; + FeType *result; + FeUnit *home=current_unit(c); + const char *want; + /* `Name(args)` here, `binding.Name(args)` when the declaration is in + another unit. */ + if (n->a->kind==FE_N_MEMBER) { + FeUnit *bound=binding_unit(s,n->a->a); + if (!bound) return unknown(c); + home=bound; + want=n->a->b->text; + } else { + want=n->a->text; + } + if (!want || !unit_type_decl(c,home,want)) return unknown(c); + if (!instance_descend(c,n->loc)) { *ok=1; return unknown(c); } + for (arg=n->children;arg;arg=arg->next) { + int inner=0; + if (countinstance_depth; return unknown(c); } + ++count; + } + if (count>FE_TYPE_PARAM_MAX) { --c->instance_depth; return unknown(c); } + result=instantiate_struct(c,home,want,args,count,n->loc); + --c->instance_depth; + *ok=1; + return result; + } + return unknown(c); +} + +/* A `comptime if` condition. Only the forms SPEC 9 allows: type equality and + the type predicates. Anything else is not decidable here. */ +int comptime_condition(FeCheckerState *s, FeNode *n, int *out) +{ + FeType *a; + FeType *b; + int ok=0; + int eq; + if (!n) return 0; + if (n->kind==FE_N_BINARY && n->text && + (strcmp(n->text,"==")==0 || strcmp(n->text,"!=")==0)) { + a=type_from_expr(s,n->a,&ok); + if (!ok) return 0; + b=type_from_expr(s,n->b,&ok); + if (!ok) return 0; + eq=fe_type_equal(a,b); + *out=strcmp(n->text,"==")==0 ? eq : !eq; + return 1; + } + if (n->kind==FE_N_CALL && n->text && + (strcmp(n->text,"@is_int")==0 || strcmp(n->text,"@is_ptr")==0)) { + a=type_from_expr(s,n->children,&ok); + if (!ok) return 0; + *out=strcmp(n->text,"@is_int")==0 ? fe_type_is_integer(a) : + (a && (a->kind==FE_TYPE_OWNED || a->kind==FE_TYPE_REF)); + return 1; + } + return 0; +} + +/* Check a generic body once, in the unit that declared it and with the + instance's arguments bound. Errors land on the operation that is wrong; the + call site gets a note, because the call is context and not the defect. */ +void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, + FeType *owner, FeBindSave *bindings, FeLoc site) +{ + FeAst *save_ast=c->ast; + FeUnit *save_unit=c->unit; + const char *save_name=c->types.unit_name; + unsigned before=c->diags->errors; + (void)bindings; + c->ast=&home->ast; + c->unit=home; + c->types.unit_name=home->name; + fe_diags_source(c->diags,home->source,home->size); + if (owner) check_method(c,decl,c->unit_scope[unit_index(c,home)],owner); + else check_fn(c,decl,c->unit_scope[unit_index(c,home)]); + c->ast=save_ast; + c->unit=save_unit; + c->types.unit_name=save_name; + if (save_unit) fe_diags_source(c->diags,save_unit->source,save_unit->size); + if (c->diags->errors>before) + fe_diag_note_src(c->diags,site,"instantiated here"); +} + +/* A call to a generic function: read the type arguments, check the value + arguments against the bound signature, then check the body once. */ +FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, + FeUnit *home) +{ + FeCheck *c=s->c; + FeNode *decl=sym->fn; + unsigned want=decl_type_param_count(decl); + FeType *args[FE_TYPE_PARAM_MAX]; + FeNode *arg=n->children; + unsigned i; + char key[FE_GENERIC_KEY_MAX]; + FeBindSave save; + FeType *result; + int fresh; + if (want>FE_TYPE_PARAM_MAX) { + err(c,n->loc,"too many generic parameters"); + return unknown(c); + } + for (i=0;iloc,"generic call requires explicit type arguments"); + return unknown(c); + } + args[i]=type_from_expr(s,arg,&ok); + if (!ok) { + err(c,arg->loc,"a comptime type argument must name a type"); + return unknown(c); + } + arg=arg->next; + } + instance_key(key,home->name,decl->text,args,want); + push_bindings(c,&save,decl,args,want); + result=check_call_args(s,n,sym,home->name,want); + fresh=instance_record(c,key,n->loc,decl,home,0); + pop_bindings(c,&save); + /* The call goes to this instance, not to the declaration it came from. */ + if (n->a) n->a->cname=(char *)instance_cname(c,key); + if (fresh>0) { + if (!instance_descend(c,n->loc)) return result; + push_bindings(c,&save,decl,args,want); + instantiate_body(c,home,decl,0,&save,n->loc); + pop_bindings(c,&save); + --c->instance_depth; + } + return result; +} + +/* `Type.method(...)` where Type is a generic instance and the method takes no + self parameter. */ +/* The unit a name belongs to, by name. */ +FeUnit *unit_named(FeCheck *c, const char *name) +{ + unsigned u; + if (!name) return 0; + for (u=0;ubuild->count;++u) + if (!strcmp(c->build->units[u].name,name)) return &c->build->units[u]; + return 0; +} + +FeType *check_static_method_call(FeCheckerState *s, FeNode *n, + FeType *owner, FeNode *method) +{ + FeCheck *c=s->c; + /* A method belongs to the unit that declared its type, not to whichever + unit happens to be calling it. */ + FeUnit *home=unit_named(c,owner ? owner->unit : 0); + FeBindSave save; + FeType *result; + char key[FE_GENERIC_KEY_MAX]; + FeType *self_args[1]; + int fresh; + FeSym fake; + if (!home) home=current_unit(c); + self_args[0]=owner; + instance_key(key,home->name,method->text,self_args,1); + memset(&fake,0,sizeof fake); + fake.name=method->text; + fake.cname=method->cname; + fake.fn=method; + fake.decl=method; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + result=check_call_args(s,n,&fake,home->name,0); + fresh=instance_record(c,key,n->loc,method,home,owner); + pop_bindings(c,&save); + if (n->a) n->a->cname=(char *)instance_cname(c,key); + if (fresh>0) { + if (!instance_descend(c,n->loc)) return result; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + instantiate_body(c,home,method,owner,&save,n->loc); + pop_bindings(c,&save); + --c->instance_depth; + } + return result; +} + +/* The body of a method on a generic instance, checked once per instance. */ +void check_instance_method(FeCheckerState *s, FeType *owner, + FeNode *method, FeLoc site, FeNode *call) +{ + FeCheck *c=s->c; + /* A method belongs to the unit that declared its type, not to whichever + unit happens to be calling it. */ + FeUnit *home=unit_named(c,owner ? owner->unit : 0); + FeBindSave save; + char key[FE_GENERIC_KEY_MAX]; + FeType *self_args[1]; + self_args[0]=owner; + if (!home) home=current_unit(c); + instance_key(key,home->name,method->text,self_args,1); + { + FeBindSave probe; + int fresh; + push_instance_bindings(c,&probe,owner); + bind_self(c,owner); + fresh=instance_record(c,key,site,method,home,owner); + pop_bindings(c,&probe); + /* The call names this instance's copy of the method. */ + if (call && call->a) call->a->cname=(char *)instance_cname(c,key); + if (fresh<=0) return; + } + if (!instance_descend(c,site)) return; + push_instance_bindings(c,&save,owner); + bind_self(c,owner); + instantiate_body(c,home,method,owner,&save,site); + pop_bindings(c,&save); + --c->instance_depth; +} + +/* SPEC 4.7: `const Word = i32;` is another spelling of a type, not a value. + It has no initializer to check and no storage. */ +int const_names_type(FeCheckerState *s, FeNode *n) +{ + FeType *t; + if (!n->b || n->b->kind!=FE_N_IDENT || !n->b->text) return 0; + if (n->a) return 0; + if (find_symbol(s->globals,n->b->text)) return 0; + t=fe_type_intern(&s->c->types,n->b->text); + return t && t->kind!=FE_TYPE_UNKNOWN; +} + +FeNode *type_method(FeType *t, const char *name) +{ + FeNode *m; + if (!t || !t->decl_node || !name) return 0; + for (m=t->decl_node->children;m;m=m->next) + if (m->kind==FE_N_FN && m->text && strcmp(m->text,name)==0) return m; + return 0; +} + +int method_is_static(const FeNode *method) +{ + FeNode *first=method && method->a ? method->a->children : 0; + return !first || !first->text || strcmp(first->text,"self")!=0; +} + +/* A call to a named function. `home` is the unit the signature was written in, + null when that is the unit being checked: parameter and return types have to + be read where they were written or a name would mean the caller's type. */ +/* `error.Name` is a member of the default error set. That set is open -- names + are collected across the build and numbered later, not declared -- so any + name is well formed here and the value's type is core.Error. */ diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h new file mode 100644 index 0000000..d3c04b7 --- /dev/null +++ b/fec/src/checkpri.h @@ -0,0 +1,252 @@ +#ifndef FE_CHECKPRI_H +#define FE_CHECKPRI_H + +/* The checker's own vocabulary, shared by the files it is split across. + Nothing outside the checker includes this. */ + +#include "check.h" +#include "m7.h" +#include + +#define FE_M7_FLOW_CAP 64U +#include "own.h" +#include +#include + +typedef struct FeSym FeSym; +/* FeScope is forward declared in check.h. */ + +struct FeSym { + const char *name; + char *cname; + FeType *type; + FeNode *fn; + int mutable; + int initialized; + int moved; + FeNode *decl; + /* M6 ownership is tracked at the root local/parameter. A reference + binding remembers that root so releasing the binding's last use can + release the root borrow without a separate alias engine. */ + FeOwnState own; + FeSym *borrow_root; + int borrow_mut; + int borrow_defer; + FeScope *owner; +}; + +struct FeScope { + FeScope *parent; + FeSym *items; + unsigned count; + unsigned capacity; +}; + + +typedef struct FeCheckerState { + FeCheck *c; + FeScope *scope; + FeScope *globals; + FeType *ret; + unsigned loop_depth; + unsigned defer_depth; + FeOwnLiveness liveness; + FeNode *fn_node; +} FeCheckerState; + +/* The type bindings in force, saved across a nested instantiation. */ +typedef struct FeBindSave { + FeTypeBind params[FE_TYPE_PARAM_MAX]; + unsigned count; +} FeBindSave; + +typedef struct FeFlowSlot { + FeSym *sym; + int moved; + int initialized; + int own_move; + int own_initialized; +} FeFlowSlot; + +typedef struct FeFlowBorrow { + FeSym *root; + int mutable; +} FeFlowBorrow; + +/* How long a chain of new generic instances may get, and how long an + instance's readable spelling may be before it falls back to serials. */ +#define FE_GENERIC_DEPTH_MAX 32 +#define FE_GENERIC_NAME_READABLE 200 + +/* Every definition in the checker, so the split files can see each other. */ +FeType *unknown(FeCheck *c); +void err(FeCheck *c, FeLoc loc, const char *msg); +int ordered_type(const FeType *t); +int known(FeType *t); +int in_own_drop(FeCheckerState *s, FeNode *n); +void mark_moved(FeCheckerState *s, FeNode *n, FeType *t); +int compatible(FeType *want, FeType *got, FeNode *value); +int call_reborrows(const FeType *param, const FeType *arg); +int explicit_castable(FeType *a, FeType *b); +FeType *node_type(FeCheck *c, FeNode *n); +char *unit_cname(FeCheck *c, const char *name); +char *local_cname(FeCheck *c, const char *name); +FeScope *scope_new(FeCheckerState *s, FeScope *parent); +FeSym *find_current(FeScope *scope, const char *name); +FeSym *find_symbol(FeScope *scope, const char *name); +FeSym *add_symbol(FeCheckerState *s, FeScope *scope, + const char *name, FeType *type, FeNode *fn, + int mutable, int initialized, char *cname, + FeNode *decl); +void enter_unit(FeCheck *c, unsigned index); +unsigned unit_index(FeCheck *c, const FeUnit *u); +FeUnit *binding_unit(FeCheckerState *s, FeNode *base); +int decl_is_public(const FeNode *decl); +FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name); +FeType *unit_type(FeCheck *c, FeUnit *u, const char *name); +FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name); +FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node); +FeNode *find_method(FeCheck *c, FeType *owner, const char *name); +FeType *method_type(FeCheck *c, FeNode *node, FeType *owner); +unsigned flow_capture(FeScope *scope, FeFlowSlot *slots, unsigned cap); +void flow_restore(FeFlowSlot *slots, unsigned count); +void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, + unsigned count); +FeSym *own_root_symbol(FeCheckerState *s, FeNode *expr); +int own_is_global(FeCheckerState *s, FeSym *sym); +void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable); +void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr); +FeSym *own_derived_call_root(FeCheckerState *s, FeNode *call); +void own_bind_derived_call(FeCheckerState *s, FeSym *binding, + FeNode *value); +int own_stmt_uses(FeNode *node, const char *name); +int own_defer_uses(FeNode *node, const char *name); +int own_contains_node(FeNode *node, FeNode *needle); +void own_release_after_stmt(FeCheckerState *s, FeScope *scope, + FeNode *stmt, int scope_end); +FeOwnState *flow_own_new(FeCheckerState *s, unsigned count); +void flow_own_capture(FeFlowSlot *slots, FeOwnState *states, + unsigned count); +void flow_own_restore(FeFlowSlot *slots, FeOwnState *states, + unsigned count); +void flow_own_merge(FeFlowSlot *slots, FeOwnState *left, + FeOwnState *right, unsigned count); +FeFlowBorrow *flow_borrow_new(FeCheckerState *s, unsigned count); +void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count); +void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, + unsigned count); +void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, + FeFlowBorrow *right, unsigned count); +FeNode *find_const_node(FeCheck *c, const char *name); +const char *builtin_format(FeCheckerState *s, FeNode *fmt); +int format_is_slice_u8(FeType *t); +int format_is_writer_type(FeType *t); +int format_arg_ok(FeType *t, int verb); +void check_format_call(FeCheckerState *s, FeNode *n); +int is_format_builtin(const char *name); +int lvalue_writable(FeCheckerState *s, FeNode *n); +int has_field(FeNode *list, const char *name); +int field_is_visible(FeCheckerState *s, const FeType *t, + const FeFieldType *field); +FeType *check_struct_fields(FeCheckerState *s, FeNode *n, FeType *t); +FeType *check_struct_init(FeCheckerState *s, FeNode *n); +FeType *check_array_init(FeCheckerState *s, FeNode *n); +int array_slice_lvalue(FeNode *n); +FeType *check_index(FeCheckerState *s, FeNode *n); +FeType *check_identifier(FeCheckerState *s, FeNode *n); +FeType *check_expr_core(FeCheckerState *s, FeNode *n); +FeType *check_lvalue_core(FeCheckerState *s, FeNode *n, int read, + FeType *base_in); +int compound_operator(const char *op); +void check_match(FeCheckerState *s, FeNode *n); +void check_for(FeCheckerState *s, FeNode *n); +void check_type_cycle(FeCheck *c, FeType *t); +void check_type_cycles(FeCheck *c); +int own_ast_reference_type(FeNode *type); +int own_ast_pointer_to_reference(FeNode *type); +void check_reference_storage(FeCheck *c, FeNode *decl); +int own_return_from_allowed_root(FeCheckerState *s, FeNode *expr); +void check_stmt_core(FeCheckerState *s, FeNode *n); +void check_fn(FeCheck *c, FeNode *fn, FeScope *globals); +void check_method(FeCheck *c, FeNode *fn, FeScope *globals, + FeType *owner); +int m7_actual_compatible(FeType *want, FeType *got, FeNode *value); +FeType *m7_check_expected(FeCheckerState *s, FeNode *value, + FeType *expected); +FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base); +int m7_place_is_projection(FeNode *n); +unsigned decl_type_param_count(const FeNode *decl); +FeNode *decl_type_param(const FeNode *decl, unsigned i); +int decl_is_generic(const FeNode *decl); +void check_generic_params(FeCheck *c, FeNode *decl); +void push_bindings(FeCheck *c, FeBindSave *save, FeNode *decl, + FeType **args, unsigned count); +void push_instance_bindings(FeCheck *c, FeBindSave *save, FeType *t); +void bind_self(FeCheck *c, FeType *owner); +void pop_bindings(FeCheck *c, const FeBindSave *save); +void instance_key(char *out, const char *unit, const char *name, + FeType **args, unsigned count); +const char *instance_cname(FeCheck *c, const char *key); +int instance_known(FeCheck *c, const char *key); +int instance_record(FeCheck *c, const char *key, FeLoc loc, + FeNode *decl, FeUnit *home, FeType *owner); +int instance_descend(FeCheck *c, FeLoc loc); +FeUnit *current_unit(FeCheck *c); +FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, + const char *key, FeType **args, + unsigned count); +FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, + FeType **args, unsigned count, FeLoc loc); +FeType *instantiate_type_node(void *owner, const FeNode *node); +FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok); +int comptime_condition(FeCheckerState *s, FeNode *n, int *out); +void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, + FeType *owner, FeBindSave *bindings, FeLoc site); +FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, + FeUnit *home); +FeUnit *unit_named(FeCheck *c, const char *name); +FeType *check_static_method_call(FeCheckerState *s, FeNode *n, + FeType *owner, FeNode *method); +void check_instance_method(FeCheckerState *s, FeType *owner, + FeNode *method, FeLoc site, FeNode *call); +int const_names_type(FeCheckerState *s, FeNode *n); +FeNode *type_method(FeType *t, const char *name); +int method_is_static(const FeNode *method); +int is_error_set_member(FeCheckerState *s, FeNode *n); +FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled); +FeType *check_call_args(FeCheckerState *s, FeNode *n, FeSym *sym, + const char *home, unsigned skip); +FeType *check_call(FeCheckerState *s, FeNode *n); +void m7_capture_flow(FeCheckerState *s, FeFlowSlot *slots, + FeOwnState **own, FeFlowBorrow **borrow, + unsigned *count); +void m7_restore_flow(FeFlowSlot *slots, FeOwnState *own, + FeFlowBorrow *borrow, unsigned count); +void m7_merge_rhs_flow(FeCheckerState *s, FeFlowSlot *base, + FeOwnState *own_base, + FeFlowBorrow *borrow_base, + unsigned count, FeFlowSlot *rhs, + FeOwnState *own_rhs, + FeFlowBorrow *borrow_rhs); +int m7_stmt_definitely_exits(FeNode *n); +FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, + FeM7LazyKind kind); +FeType *check_expr(FeCheckerState *s, FeNode *n); +FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read); +FeType *m7_pattern_binding_type(FeCheckerState *s, FeType *payload, + FeNode *source, int *borrow_mut); +void m7_check_if_let(FeCheckerState *s, FeNode *n); +void m7_check_optional_match(FeCheckerState *s, FeNode *n, + FeType *opt); +void m7_check_match_stmt(FeCheckerState *s, FeNode *n); +void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable); +void check_stmt(FeCheckerState *s, FeNode *n); +int m7_ast_reference_storage(FeNode *type); +void m7_check_storage(FeCheck *c, FeNode *decl); +void m7_validate_error_decl(FeCheck *c, FeNode *decl); +void declare_unit(FeCheck *c); +FeScope *declare_unit_scope(FeCheck *c, FeCheckerState *s); +void check_unit_bodies(FeCheck *c, FeCheckerState *s); + +#endif diff --git a/fec/src/checkpro.c b/fec/src/checkpro.c new file mode 100644 index 0000000..74acf81 --- /dev/null +++ b/fec/src/checkpro.c @@ -0,0 +1,182 @@ +#include "checkpri.h" + +int m7_ast_reference_storage(FeNode *type) +{ + if (!type || !type->text) return 0; + if (strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || + (strcmp(type->text,"[")==0 && !type->a) || + strcmp(type->text,"str")==0) + return 1; + if (strcmp(type->text,"?")==0) + return m7_ast_reference_storage(type->a); + if (strcmp(type->text,"^")==0) return 0; + return 0; +} + +void m7_check_storage(FeCheck *c, FeNode *decl) +{ + FeNode *m; + if (!decl) return; + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + /* This pass exists for the shapes the other one cannot see, such as a + reference behind an optional. A plain `&T` field is seen by both, so + leave that one to check_reference_storage below. */ + for (m=decl->children;m;m=m->next) + if (m->kind==FE_N_FIELD && m7_ast_reference_storage(m->a) && + !own_ast_reference_type(m->a) && + !own_ast_pointer_to_reference(m->a)) + err(c,m->loc,"reference type is not allowed in aggregate storage"); + } + check_reference_storage(c,decl); +} + +void m7_validate_error_decl(FeCheck *c, FeNode *decl) +{ + FeNode *a; + FeNode *b; + unsigned long code; + unsigned long other; + if (!decl || decl->kind!=FE_N_ERROR_DECL) return; + for (a=decl->children;a;a=a->next) { + if (!a->a || a->a->kind!=FE_N_LITERAL || !a->a->text) continue; + code=strtoul(a->a->text,0,0); + if (code==0UL) + err(c,a->loc,"error code 0 is reserved for success"); + for (b=decl->children;b && b!=a;b=b->next) { + if (a->text && b->text && strcmp(a->text,b->text)==0) { + err(c,a->loc,"duplicate error member name"); + break; + } + if (b->a && b->a->kind==FE_N_LITERAL && b->a->text) { + other=strtoul(b->a->text,0,0); + if (other==code) { + err(c,a->loc,"duplicate error numeric code"); + break; + } + } + } + } +} + +/* Everything a unit declares, before any body anywhere is looked at. */ +void declare_unit(FeCheck *c) +{ + FeNode *n; + /* A generic declaration is not a type; only its instances are. */ + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) + fe_type_declare_struct(&c->types,n,(n->flags & FE_NODE_PACKED)!=0); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + FeNode *m; + m7_check_storage(c,n); + if (n->kind==FE_N_ERROR_DECL) m7_validate_error_decl(c,n); + check_generic_params(c,n); + for (m=n->kind==FE_N_STRUCT ? n->children : 0;m;m=m->next) + if (m->kind==FE_N_FN) check_generic_params(c,m); + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ENUM && !decl_is_generic(n)) + fe_type_declare_enum(&c->types,n); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); + check_type_cycles(c); +} + +/* The unit's top-level names, in a scope of their own so that another unit + can look into it later without inheriting anything else. */ +FeScope *declare_unit_scope(FeCheck *c, FeCheckerState *s) +{ + FeNode *n; + FeNode *m; + FeType *t; + FeScope *globals; + char method_name[128]; + globals=scope_new(s,0); + s->scope=globals; + s->globals=globals; + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) { + if (n->kind==FE_N_STRUCT) { + for (m=n->children;m;m=m->next) if (m->kind==FE_N_FN) { + sprintf(method_name,"%s_%s",n->text ? n->text : "Type", + m->text ? m->text : "method"); + m->cname=unit_cname(c,method_name); + } + } + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + t=n->a ? node_type(c,n->a) : unknown(c); + add_symbol(s,globals,n->text,t,0,n->kind==FE_N_GLOBAL, + n->b!=0,unit_cname(c,n->text ? n->text : "global"),n); + } + } + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN) { + t=fe_type_intern(&c->types,""); + /* `extern "c"` means the linker already knows this name, so it is + not decorated with the unit it was declared in. */ + add_symbol(s,globals,n->text,t,n,0,1, + (n->flags & FE_NODE_EXTERN) && n->text ? n->text : + unit_cname(c,n->text ? n->text : "fn"),n); + } + return globals; +} + +void check_unit_bodies(FeCheck *c, FeCheckerState *s) +{ + FeNode *n; + FeNode *m; + FeSym *sym; + FeType *t; + FeType *iv; + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_GLOBAL || n->kind==FE_N_CONST) { + sym=find_current(s->globals,n->text ? n->text : ""); + if (n->kind==FE_N_CONST && const_names_type(s,n)) continue; + if (n->b) { + iv=m7_check_expected(s,n->b,sym ? sym->type : 0); + if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { + sym->type=iv; + n->sem_type=iv; + } else if (sym && !fe_type_equal(sym->type,iv) && + !m7_actual_compatible(sym->type,iv,n->b)) + err(c,n->loc,"global initializer type mismatch"); + } + } + /* A generic body means nothing until its parameters are bound, so it is + checked once per instance and not here. */ + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_FN && !decl_is_generic(n)) check_fn(c,n,s->globals); + for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) + if (n->kind==FE_N_STRUCT && !decl_is_generic(n)) { + t=fe_type_intern(&c->types,n->text); + for (m=n->children;m;m=m->next) + if (m->kind==FE_N_FN) check_method(c,m,s->globals,t); + } +} + +int fe_check_program(FeCheck *c) +{ + FeCheckerState s; + unsigned u; + s.c=c; + s.scope=0; + s.globals=0; + s.ret=fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=0; + fe_own_liveness_init(&s.liveness,&c->arena); + for (u=0;ubuild->count;++u) { enter_unit(c,u); declare_unit(c); } + fe_type_layout_all(&c->types); + for (u=0;ubuild->count;++u) { + enter_unit(c,u); + c->unit_scope[u]=declare_unit_scope(c,&s); + } + for (u=0;ubuild->count;++u) { + enter_unit(c,u); + s.scope=c->unit_scope[u]; + s.globals=c->unit_scope[u]; + check_unit_bodies(c,&s); + } + fe_type_layout_all(&c->types); + return c->diags->errors==0; +} diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c new file mode 100644 index 0000000..587e199 --- /dev/null +++ b/fec/src/checkstm.c @@ -0,0 +1,635 @@ +#include "checkpri.h" + +void check_match(FeCheckerState *s, FeNode *n) +{ + FeType *value; + FeNode *arm; + FeVariantType *variant; + int seen[256]; + int wildcard=0; + FeFlowSlot base[64], merged[64], current[64]; + unsigned flow_count; + int have_merged=0; + unsigned i; + for(i=0;i<256U;i++) seen[i]=0; + value=check_expr(s,n->a); + if(!value || value->kind!=FE_TYPE_ENUM) { err(s->c,n->loc,"match requires an enum value"); return; } + flow_count=flow_capture(s->scope,base,64); + for(arm=n->children;arm;arm=arm->next) { + FeScope *old=s->scope; + flow_restore(base,flow_count); + if(arm->text && strcmp(arm->text,"_")==0) wildcard=1; + else { + variant=fe_type_variant(value,arm->text); + if(!variant) { err(s->c,arm->loc,"unknown match variant"); continue; } + if(variant->tag<256U) { + if(seen[variant->tag]) err(s->c,arm->loc,"duplicate match variant"); + seen[variant->tag]=1; + } + s->scope=scope_new(s,old); + if(variant->field_count==1 && arm->children) { + add_symbol(s,s->scope,arm->children->text,variant->fields[0].type,0,0,1, + local_cname(s->c,arm->children->text),arm->children); + } else if(variant->field_count>0) { + FeNode *b=arm->children; + for(i=0;ifield_count && b;i++,b=b->next) { + FeFieldType *f=&variant->fields[i]; + add_symbol(s,s->scope,b->text,f->type,0,0,1, + local_cname(s->c,b->text),b); + } + } + } + if(arm->a && arm->a->kind==FE_N_BLOCK) check_stmt(s,arm->a); + else if(arm->a) check_expr(s,arm->a); + s->scope=old; + flow_capture(s->scope,current,flow_count); + if(!have_merged) { + for(i=0;ivariant_count && i<256U;i++) if(!seen[i]) err(s->c,n->loc,"non-exhaustive match"); +} + +void check_for(FeCheckerState *s, FeNode *n) +{ + FeType *start; + FeType *finish; + FeType *elem; + FeType *ref_type; + FeSym *iter_sym; + char *index_cname; + char *item_cname; + int iter_mut; + FeScope *old=s->scope; + if(!n->c) { + start=check_expr(s,n->a); + if (!fe_type_is_indexable(start)) { + err(s->c,n->loc,"for iterable must be an array, slice, or str"); + return; + } + elem=start->elem; + iter_sym=0; + if (n->a && n->a->kind==FE_N_IDENT) + iter_sym=find_symbol(s->scope,n->a->text ? n->a->text : ""); + else if (n->a && n->a->kind==FE_N_INDEX && n->a->a && + n->a->a->kind==FE_N_IDENT) + iter_sym=find_symbol(s->scope,n->a->a->text ? n->a->a->text : ""); + iter_mut=start->kind==FE_TYPE_SLICE ? start->ref_mut : + (iter_sym && iter_sym->mutable); + ref_type=fe_type_ref(&s->c->types,elem,iter_mut); + if (iter_mut) n->flags |= 4U; + s->scope=scope_new(s,old); + if (n->aux_text) { + index_cname=local_cname(s->c,n->text ? n->text : "index"); + item_cname=local_cname(s->c,n->aux_text); + add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, + index_cname,n); + add_symbol(s,s->scope,n->aux_text,ref_type,0,iter_mut,1, + item_cname,0); + n->cname=index_cname; + n->aux_cname=item_cname; + } else { + item_cname=local_cname(s->c,n->text ? n->text : "item"); + add_symbol(s,s->scope,n->text,ref_type,0,iter_mut,1, + item_cname,n); + n->cname=item_cname; + } + check_stmt(s,n->b); + s->scope=old; + return; + } + start=check_expr(s,n->a); + finish=check_expr(s,n->c); + if(known(start)&&!fe_type_is_integer(start)) err(s->c,n->loc,"range start must be integer"); + if(known(finish)&&!fe_type_is_integer(finish)) err(s->c,n->loc,"range end must be integer"); + s->scope=scope_new(s,old); + index_cname=local_cname(s->c,n->text ? n->text : "index"); + add_symbol(s,s->scope,n->text,fe_type_intern(&s->c->types,"usize"),0,0,1, + index_cname,n); + n->cname=index_cname; + check_stmt(s,n->b); + s->scope=old; +} + +void check_type_cycle(FeCheck *c, FeType *t) +{ + unsigned i; + FeType *next; + if (!t || t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR || + t->kind == FE_TYPE_REF || t->kind == FE_TYPE_OWNED || + t->kind == FE_TYPE_INT || t->kind == FE_TYPE_BOOL || + t->kind == FE_TYPE_CHAR || t->kind == FE_TYPE_VOID || + t->kind == FE_TYPE_UNKNOWN || t->kind == FE_TYPE_ERROR) return; + if (t->kind == FE_TYPE_ERROR_UNION) { + check_type_cycle(c,t->error_value); + return; + } + if (t->cycle_state == 1) { + if (c->ast->root) err(c, c->ast->root->loc, "by-value recursive type"); + return; + } + if (t->cycle_state == 2) return; + t->cycle_state = 1; + if (t->kind == FE_TYPE_ARRAY) { + check_type_cycle(c,t->elem); + } else if (t->kind == FE_TYPE_STRUCT) { + for (i=0;ifield_count;i++) { + if (!t->fields[i].type && t->fields[i].ast_node) + t->fields[i].type=fe_type_from_ast(&c->types,t->fields[i].ast_node->a); + check_type_cycle(c,t->fields[i].type); + } + } else if (t->kind == FE_TYPE_ENUM) { + for (i=0;ivariant_count;i++) { + unsigned j; + for (j=0;jvariants[i].field_count;j++) { + if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) + t->variants[i].fields[j].type=fe_type_from_ast(&c->types, + t->variants[i].fields[j].ast_node->a); + next=t->variants[i].fields[j].type; + check_type_cycle(c,next); + } + } + } + t->cycle_state=2; +} + +void check_type_cycles(FeCheck *c) +{ + FeType *t; + for (t=c->types.types;t;t=t->next) t->cycle_state=0; + for (t=c->types.types;t;t=t->next) check_type_cycle(c,t); +} + +int own_ast_reference_type(FeNode *type) +{ + if (!type || !type->text) return 0; + return strcmp(type->text,"&")==0 || strcmp(type->text,"&mut")==0 || + (strcmp(type->text,"[")==0 && !type->a) || strcmp(type->text,"str")==0; +} + +int own_ast_pointer_to_reference(FeNode *type) +{ + return type && type->text && strcmp(type->text,"*")==0 && + own_ast_reference_type(type->a); +} + +void check_reference_storage(FeCheck *c, FeNode *decl) +{ + FeNode *m; + if (!decl) return; + if (decl->kind==FE_N_STRUCT || decl->kind==FE_N_ENUM) { + for (m=decl->children;m;m=m->next) + if (m->kind==FE_N_FIELD && + (own_ast_reference_type(m->a) || own_ast_pointer_to_reference(m->a))) + err(c,m->loc,"reference type is not allowed in aggregate storage"); + } + if ((decl->kind==FE_N_GLOBAL || decl->kind==FE_N_CONST) && decl->a && + own_ast_reference_type(decl->a) && + !(decl->kind==FE_N_CONST && decl->a->text && strcmp(decl->a->text,"str")==0)) + err(c,decl->loc,"reference type is not allowed in global storage"); + if (decl->kind==FE_N_FN && decl->b && own_ast_pointer_to_reference(decl->b)) + err(c,decl->b->loc,"reference type is not allowed as a pointer target"); + if (decl->kind==FE_N_FN) + for (m=decl->a ? decl->a->children : 0;m;m=m->next) + if (own_ast_pointer_to_reference(m->a)) + err(c,m->loc,"reference type is not allowed as a pointer target"); +} + +int own_return_from_allowed_root(FeCheckerState *s, FeNode *expr) +{ + FeSym *root; + FeNode *p; + unsigned refs=0; + if (!expr) return 0; + root=own_root_symbol(s,expr); + if (!root) return 1; /* Static-producing builtins/methods are checked by + their declared R8 interface. */ + if (own_is_global(s,root)) + return root->decl && root->decl->kind==FE_N_GLOBAL && + (root->decl->flags & 2U); + if (!root->decl || root->decl->kind!=FE_N_PARAM) return 0; + for (p=s->fn_node && s->fn_node->a ? s->fn_node->a->children : 0; + p;p=p->next) { + FeType *t=p->sem_type ? p->sem_type : node_type(s->c,p->a); + if (fe_own_is_reference_like(t)) ++refs; + } + if (s->fn_node && s->fn_node->text && refs && + root->name && strcmp(root->name,"self")==0) return 1; + return refs==1; +} + +void check_stmt_core(FeCheckerState *s, FeNode *n) +{ + FeCheck *c = s->c; + FeScope *old; + FeType *a; + FeType *b; + FeSym *sym; + FeNode *x; + int initialized; + if (!n) return; + switch (n->kind) { + case FE_N_BLOCK: + old = s->scope; + s->scope = scope_new(s, old); + for (x = n->children; x; x = x->next) { + check_stmt(s,x); + own_release_after_stmt(s,s->scope,x,0); + } + own_release_after_stmt(s,s->scope,n,1); + s->scope = old; + break; + case FE_N_LET: + case FE_N_CONST: + a = n->a ? node_type(c, n->a) : unknown(c); + b = check_expr(s, n->b); + if (!n->a) a = b; + if (a->kind == FE_TYPE_VOID) + err(c, n->loc, "variable cannot have void type"); + if (n->a && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "initializer type mismatch"); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "void expression cannot initialize a variable"); + if (n->kind==FE_N_LET && a->kind==FE_TYPE_SLICE && a->ref_mut) + err(c,n->loc,"let cannot bind a mutable slice"); + mark_moved(s,n->b,b); + sym=add_symbol(s, s->scope, n->text, a, 0, 0, 1, + local_cname(c, n->text ? n->text : "local"), n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth != 0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); + break; + case FE_N_VAR: + a = n->a ? node_type(c, n->a) : unknown(c); + if (!n->b && !n->a) + err(c, n->loc, "uninitialized var requires an explicit type"); + b = n->b ? check_expr(s, n->b) : unknown(c); + if (!n->a && n->b) a = b; + if (a->kind == FE_TYPE_VOID) + err(c, n->loc, "variable cannot have void type"); + if (n->b && !compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "initializer type mismatch"); + if (b->kind == FE_TYPE_VOID) + err(c, n->loc, "void expression cannot initialize a variable"); + mark_moved(s,n->b,b); + initialized = n->b != 0; + sym=add_symbol(s, s->scope, n->text, a, 0, 1, initialized, + local_cname(c, n->text ? n->text : "local"), n); + if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { + sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + sym->borrow_defer=s->defer_depth != 0 || + own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); + } + own_bind_derived_call(s,sym,n->b); + break; + case FE_N_ASSIGN: + b = check_expr(s, n->b); + a = check_lvalue(s, n->a, compound_operator(n->text)); + if (!compatible(a, b, n->b) && b->kind != FE_TYPE_UNKNOWN) + err(c, n->loc, "assignment type mismatch"); + mark_moved(s,n->b,b); + sym = n->a && n->a->kind == FE_N_IDENT ? + find_symbol(s->scope, n->a->text) : 0; + if (sym && sym->mutable) { + sym->initialized = 1; + fe_own_access(s->c->diags,&sym->own,FE_OWN_WRITE,n->a->loc); + sym->moved=sym->own.move; + if (n->b && n->b->kind==FE_N_UNARY && n->b->text && + (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0) && + fe_own_is_reference_like(sym->type)) { + FeSym *root=own_root_symbol(s,n->b->a); + if (root && root->owner!=sym->owner) + err(c,n->b->loc,"reference would outlive its source scope"); + else if (root) { + if (sym->borrow_root) { + if (sym->borrow_mut) fe_own_release_exclusive(&sym->borrow_root->own); + else fe_own_release_shared(&sym->borrow_root->own); + } + sym->borrow_root=root; + sym->borrow_mut=strcmp(n->b->text,"&mut")==0; + } + } + } + break; + case FE_N_EXPR_STMT: + /* The enclosing-error-result check lives on the try expression itself, + so a bare `try e;` needs nothing extra here. */ + check_expr(s, n->a); + break; + case FE_N_DEFER: + ++s->defer_depth; + check_stmt(s,n->a); + --s->defer_depth; + break; + case FE_N_IF: { + FeFlowSlot base[64], left[64], right[64]; + FeOwnState *own_base, *own_left, *own_right; + FeFlowBorrow *borrow_base, *borrow_left, *borrow_right; + unsigned flow_count; + a = check_expr(s, n->a); + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "if condition must be bool"); + flow_count=flow_capture(s->scope,base,64); + own_base=flow_own_new(s,flow_count); + own_left=flow_own_new(s,flow_count); + own_right=flow_own_new(s,flow_count); + borrow_base=flow_borrow_new(s,flow_count); + borrow_left=flow_borrow_new(s,flow_count); + borrow_right=flow_borrow_new(s,flow_count); + flow_own_capture(base,own_base,flow_count); + flow_borrow_capture(base,borrow_base,flow_count); + check_stmt(s, n->b); + flow_capture(s->scope,left,flow_count); + flow_own_capture(left,own_left,flow_count); + flow_borrow_capture(left,borrow_left,flow_count); + flow_restore(base,flow_count); + flow_own_restore(base,own_base,flow_count); + flow_borrow_restore(base,borrow_base,flow_count); + if (n->c) check_stmt(s, n->c); + if (n->c) { + flow_capture(s->scope,right,flow_count); + flow_own_capture(right,own_right,flow_count); + flow_borrow_capture(right,borrow_right,flow_count); + } + else { + unsigned i; + for (i=0;ia); + if (known(a) && a->kind != FE_TYPE_BOOL) + err(c, n->loc, "while condition must be bool"); + flow_count=flow_capture(s->scope,base,64); + own_base=flow_own_new(s,flow_count); + own_body=flow_own_new(s,flow_count); + own_entry2=flow_own_new(s,flow_count); + borrow_base=flow_borrow_new(s,flow_count); + borrow_body=flow_borrow_new(s,flow_count); + borrow_entry2=flow_borrow_new(s,flow_count); + flow_own_capture(base,own_base,flow_count); + flow_borrow_capture(base,borrow_base,flow_count); + if (s->loop_depth < 255U) ++s->loop_depth; + check_stmt(s, n->b); + if (s->loop_depth) --s->loop_depth; + flow_capture(s->scope,body,flow_count); + flow_own_capture(body,own_body,flow_count); + flow_borrow_capture(body,borrow_body,flow_count); + for (i=0;iloop_depth < 255U) ++s->loop_depth; + check_stmt(s,n->b); + if (s->loop_depth) --s->loop_depth; + flow_capture(s->scope,body,flow_count); + flow_own_capture(body,own_body,flow_count); + flow_borrow_capture(body,borrow_body,flow_count); + for(i=0;iloop_depth < 255U) ++s->loop_depth; + check_for(s,n); + if (s->loop_depth) --s->loop_depth; + break; + case FE_N_MATCH: + check_match(s,n); + break; + case FE_N_BREAK: + case FE_N_CONTINUE: + if (!s->loop_depth) err(c,n->loc,"break or continue outside loop"); + break; + case FE_N_RETURN: + b = n->a ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); + if (s->ret && fe_own_is_reference_like(s->ret) && + !own_return_from_allowed_root(s,n->a)) + err(c,n->loc,"reference return must be derived from a parameter or static"); + mark_moved(s,n->a,b); + if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) + err(c, n->loc, "void expression returned from value function"); + else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && + b->kind != FE_TYPE_UNKNOWN && + !compatible(s->ret,b,n->a)) + err(c, n->loc, "return type mismatch"); + break; + case FE_N_UNSAFE: + check_stmt(s, n->a); + break; + default: + break; + } +} + +void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) +{ + FeCheckerState s; + FeScope *old; + FeNode *x; + FeType *t; + s.c = c; + s.globals = globals; + s.scope = scope_new(&s, globals); + s.ret = fn->b ? node_type(c, fn->b) : fe_type_intern(&c->types, "void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->arena); + fe_own_collect_last_uses(&s.liveness,fn); + fn->sem_type = s.ret; + for (x = fn->a ? fn->a->children : 0; x; x = x->next) { + t = node_type(c, x->a); + if (t->kind == FE_TYPE_VOID) + err(c, x->loc, "parameter cannot have void type"); + add_symbol(&s, s.scope, x->text, t, 0, 1, 1, + local_cname(c, x->text ? x->text : "arg"), x); + } + old = s.scope; + if (fn->c) check_stmt(&s, fn->c); + s.scope = old; +} + +void check_method(FeCheck *c, FeNode *fn, FeScope *globals, + FeType *owner) +{ + FeCheckerState s; + FeNode *x; + FeType *t; + s.c=c; + s.globals=globals; + s.scope=scope_new(&s,globals); + s.ret=fn->b ? method_type(c,fn->b,owner) : fe_type_intern(&c->types,"void"); + s.loop_depth=0; + s.defer_depth=0; + s.fn_node=fn; + fe_own_liveness_init(&s.liveness,&c->arena); + fe_own_collect_last_uses(&s.liveness,fn); + fn->sem_type=s.ret; + for(x=fn->a ? fn->a->children : 0; x; x=x->next) { + t=method_type(c,x->a,owner); + x->sem_type=t; + add_symbol(&s,s.scope,x->text,t,0,1,1, + local_cname(c,x->text ? x->text : "arg"),x); + } + if(fn->c) check_stmt(&s,fn->c); +} + +int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) +{ + if (fe_type_equal(want,got)) return 1; + return compatible(want,got,value); +} + +FeType *m7_check_expected(FeCheckerState *s, FeNode *value, + FeType *expected) +{ + FeType *actual; + FeM7ContextKind context; + if (!value) return unknown(s->c); + if (fe_m7_is_null(value)) { + if (!fe_m7_can_contextual_null(expected)) { + err(s->c,value->loc,"null requires a contextual optional type"); + value->sem_type=unknown(s->c); + return value->sem_type; + } + value->sem_type=expected; + value->sem_context=expected; + return expected; + } + actual=check_expr(s,value); + if (!expected) return actual; + if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && + m7_actual_compatible(expected->elem,actual,value)) { + value->sem_context=expected; + return expected; + } + if (expected->kind==FE_TYPE_ERROR_UNION) { + context=fe_m7_error_context(&s->c->types,expected,actual); + if (context!=FE_M7_CONTEXT_NONE) { + value->sem_context=expected; + return expected; + } + } + return actual; +} + +FeType *m7_member_field(FeCheckerState *s, FeNode *n, FeType *base) +{ + FeFieldType *field; + FeType *owner; + if (!base) return unknown(s->c); + if (n->text && strcmp(n->text,".?")==0) { + if (base->kind!=FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional projection '.?' requires an optional value"); + return unknown(s->c); + } + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OPTIONAL) { + err(s->c,n->loc,"optional value must be projected with '.?' first"); + return unknown(s->c); + } + if (base->kind==FE_TYPE_REF && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + if (base->kind==FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) { + n->sem_type=base->elem; + return n->sem_type; + } + owner=base; + if ((base->kind==FE_TYPE_REF || base->kind==FE_TYPE_OWNED) && + base->elem && base->elem->kind==FE_TYPE_STRUCT) + owner=base->elem; + if (owner && owner->kind==FE_TYPE_STRUCT && n->b && n->b->text) { + field=fe_type_field(owner,n->b->text); + if (!field) { + err(s->c,n->loc,"unknown struct field"); + return unknown(s->c); + } + n->sem_type=field->type; + return field->type; + } + if (base->kind==FE_TYPE_ENUM && n->b && n->b->text) { + if (!fe_type_variant(base,n->b->text)) + err(s->c,n->loc,"unknown enum variant"); + n->sem_type=base; + return base; + } + if ((base->kind==FE_TYPE_SLICE || base->kind==FE_TYPE_STR) && + n->b && n->b->text && strcmp(n->b->text,"n")==0) { + n->sem_type=fe_type_intern(&s->c->types,"usize"); + return n->sem_type; + } + n->sem_type=unknown(s->c); + return n->sem_type; +} + +int m7_place_is_projection(FeNode *n) +{ + return n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX); +} + +/* ------------------------------------------------------------------------- * + * Generics (SPEC 9) + * + * A generic declaration is checked once per distinct list of type arguments. + * Those arguments are bound as types for the length of that check, so a name + * that is a type parameter simply is its argument -- in the body, in field + * types and in the signature alike. An instance is identified by its declaring + * unit, its declaration and the spelling of its arguments, so asking twice + * asks for the same instance, and a chain of new ones is bounded. + * ------------------------------------------------------------------------- */ + diff --git a/fec/src/lower.c b/fec/src/lower.c index 07c2e08..da94f4d 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -1,103 +1,6 @@ -#include "lower.h" -#include -#include "m7.h" -#include "own.h" -#include +#include "lowerpri.h" -/* ------------------------------------------------------------------------- * - * Lowering - * - * One function at a time, one statement at a time. A `Slot` is what an - * expression produced: either a value already in a temporary, or a place in - * memory that a value can be read from or written to. Aggregates are always - * places -- they are never carried in a temporary, because a temporary is a - * register and an aggregate does not fit in one. - * ------------------------------------------------------------------------- */ - -#define LOWER_MAX_LOCALS 256 - -typedef struct LowerVar { - const char *cname; - unsigned local; - /* An aggregate parameter arrives as an address, so the slot holds a - pointer and the value is one dereference away. */ - int by_address; -} LowerVar; - -typedef struct Lower { - FeCheck *c; - FeIrModule *m; - FeIrFunc *fn; - FeIrBlock *b; /* the block being appended to */ - FeType *ret_type; - unsigned ret_local; /* hidden result address, when returning mem */ - LowerVar vars[LOWER_MAX_LOCALS]; - unsigned var_count; - /* Loop targets, for break and continue. */ - unsigned break_target[32]; - unsigned continue_target[32]; - unsigned loop_depth; - /* What a scope still owes when it ends: `defer` blocks to run and owned - values to release, in the order they were written. Every exit path runs - what is live, last first. - - A drop carries a flag beside the value. The flag is set when the value - is stored and cleared wherever it is moved away, so the release happens - exactly on the paths where the value is still there -- which is not - something the shape of the code can tell you on its own. */ - struct { - FeNode *block; /* a `defer`, when set */ - unsigned local; /* the owned value, otherwise */ - unsigned flag; - FeType *type; - } owed[64]; - unsigned owed_count; - /* Every `error.Name` used anywhere in the build, sorted, numbered from one. - SPEC 4.6: the names are collected rather than declared, and the order is - fixed by the spelling so that the same program always gets the same - codes however the build was ordered. */ - const char *error_names[256]; - unsigned error_count; - int failed; -} Lower; - -typedef struct Slot { - int is_place; - unsigned temp; /* the value, when is_place is 0 */ - FeIrPlace place; /* where it lives, when is_place is 1 */ - FeIrType type; - unsigned long size; /* for FE_IR_MEM */ -} Slot; - -static Slot lower_expr(Lower *L, FeNode *n); -static void lower_stmt(Lower *L, FeNode *n); -static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, - unsigned long size); -static void lower_for(Lower *L, FeNode *n); -static int lower_mem(Lower *L, FeNode *n, Slot *out); -static long error_code(Lower *L, const char *name); -static int fn_is_generic(const FeNode *fn); -static void lower_fn_as(Lower *L, FeNode *fn, const char *name); -static Slot lower_slice(Lower *L, FeNode *n); -static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line); -static void lower_match(Lower *L, FeNode *n); -static int enum_has_payload(const FeType *t); -static void lower_global(Lower *L, FeNode *n); -static long literal_value(FeNode *n); -static Slot wrap_context(Lower *L, Slot v, FeNode *n); -static Slot lower_try(Lower *L, FeNode *n); -static Slot lower_lazy(Lower *L, FeNode *n, int is_catch); -static Slot wrapper_payload(Lower *L, Slot w, const FeType *t); -static unsigned scratch(Lower *L, const FeType *t, const char *why); -static int uses_niche(const FeType *t); -static FeIrType tag_type(const FeType *t); -static void run_deferred(Lower *L, unsigned from); -static unsigned declare_var(Lower *L, const char *cname, const FeType *t, - const char *name); -static void indexable_parts(Lower *L, Slot base, const FeType *t, - unsigned *data, unsigned *length, FeNode *n); - -static void fail(Lower *L, const char *why, FeNode *n) +void fail(Lower *L, const char *why, FeNode *n) { if (L->failed) return; L->failed = 1; @@ -111,7 +14,7 @@ static void fail(Lower *L, const char *why, FeNode *n) /* A Ferro type becomes what a register can hold, or a size in memory. Anything with more than one field is memory: the backend never has to decide whether an aggregate fits somewhere. */ -static FeIrType ir_type_of(const FeType *t) +FeIrType ir_type_of(const FeType *t) { if (!t) return FE_IR_VOID; switch (t->kind) { @@ -136,7 +39,7 @@ static FeIrType ir_type_of(const FeType *t) } } -static int enum_has_payload(const FeType *t) +int enum_has_payload(const FeType *t) { unsigned i; if (!t || t->kind != FE_TYPE_ENUM) return 0; @@ -145,30 +48,30 @@ static int enum_has_payload(const FeType *t) return 0; } -static FeIrType ir_type(const FeType *t) +FeIrType ir_type(const FeType *t) { if (t && t->kind == FE_TYPE_ENUM && enum_has_payload(t)) return FE_IR_MEM; return ir_type_of(t); } -static unsigned long ir_size(const FeType *t) +unsigned long ir_size(const FeType *t) { return t ? fe_type_size(t) : 0UL; } -static unsigned ir_align(const FeType *t) +unsigned ir_align(const FeType *t) { return t ? fe_type_align(t) : 1U; } -static int type_is_unsigned(const FeType *t) +int type_is_unsigned(const FeType *t) { return t && t->kind == FE_TYPE_INT && t->is_unsigned; } /* ---------------------------------------------------------------- slots --- */ -static Slot slot_value(unsigned temp, FeIrType t) +Slot slot_value(unsigned temp, FeIrType t) { Slot s; s.is_place = 0; s.temp = temp; s.type = t; s.size = 0; @@ -176,21 +79,21 @@ static Slot slot_value(unsigned temp, FeIrType t) return s; } -static Slot slot_place(FeIrPlace p, FeIrType t, unsigned long size) +Slot slot_place(FeIrPlace p, FeIrType t, unsigned long size) { Slot s; s.is_place = 1; s.temp = 0; s.place = p; s.type = t; s.size = size; return s; } -static Slot slot_void(void) +Slot slot_void(void) { return slot_value(0, FE_IR_VOID); } /* Read a slot as a value. An aggregate has no value form, so asking for one is a lowering bug rather than a program error. */ -static unsigned as_value(Lower *L, Slot s, FeNode *n) +unsigned as_value(Lower *L, Slot s, FeNode *n) { if (!s.is_place) return s.temp; if (s.type == FE_IR_MEM) { fail(L, "an aggregate as a value", n); return 0; } @@ -198,7 +101,7 @@ static unsigned as_value(Lower *L, Slot s, FeNode *n) } /* The address of a slot. */ -static unsigned as_address(Lower *L, Slot s, FeNode *n) +unsigned as_address(Lower *L, Slot s, FeNode *n) { if (!s.is_place) { fail(L, "the address of a temporary", n); return 0; } return fe_ir_addr(L->m, L->b, s.place); @@ -207,14 +110,14 @@ static unsigned as_address(Lower *L, Slot s, FeNode *n) /* --------------------------------------------------------------- locals --- */ /* Does letting go of this type have to do something? */ -static int needs_release(const FeType *t) +int needs_release(const FeType *t) { if (!t) return 0; if (t->kind == FE_TYPE_OWNED) return 1; return t->has_drop != 0; } -static unsigned declare_var(Lower *L, const char *cname, const FeType *t, +unsigned declare_var(Lower *L, const char *cname, const FeType *t, const char *name) { unsigned local = fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), @@ -239,7 +142,7 @@ static unsigned declare_var(Lower *L, const char *cname, const FeType *t, } /* The liveness flag beside a local, or none. */ -static int release_flag(Lower *L, unsigned local, unsigned *flag) +int release_flag(Lower *L, unsigned local, unsigned *flag) { unsigned i; for (i = L->owed_count; i > 0; --i) @@ -250,7 +153,7 @@ static int release_flag(Lower *L, unsigned local, unsigned *flag) return 0; } -static LowerVar *find_var(Lower *L, const char *cname) +LowerVar *find_var(Lower *L, const char *cname) { unsigned i; if (!cname) return 0; @@ -262,7 +165,7 @@ static LowerVar *find_var(Lower *L, const char *cname) /* --------------------------------------------------------------- blocks --- */ -static FeIrBlock *new_block(Lower *L) +FeIrBlock *new_block(Lower *L) { return fe_ir_block(L->m, L->fn); } @@ -270,7 +173,7 @@ static FeIrBlock *new_block(Lower *L) /* A check that must hold. `ok` is a condition; when it is false the program stops where it is. `--no-checks` removes the comparison and the branch, not just the message, which is the whole point of the flag. */ -static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) +void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) { FeIrBlock *bad = new_block(L); FeIrBlock *cont = new_block(L); @@ -283,30 +186,26 @@ static void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) /* A tag says which of the two things a wrapper holds. An optional is one byte at the front unless the payload has a spare representation; an error union is a two-byte error code, and zero means there is no error. */ -static FeIrType tag_type(const FeType *t) +FeIrType tag_type(const FeType *t) { return t && t->kind == FE_TYPE_ERROR_UNION ? FE_IR_I16 : FE_IR_I8; } -static int uses_niche(const FeType *t) +int uses_niche(const FeType *t) { return t && t->kind == FE_TYPE_OPTIONAL && fe_m7_optional_uses_niche(t->elem); } /* Somewhere to build an aggregate that has no home of its own yet. */ -static unsigned scratch(Lower *L, const FeType *t, const char *why) +unsigned scratch(Lower *L, const FeType *t, const char *why) { return fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), ir_align(t), why); } -/* A slice is a pointer and a length, in that order. Both the compiler and the - runtime read it this way, so the offsets live here and nowhere else. */ -#define SLICE_PTR_OFFSET 0L -#define SLICE_LEN_OFFSET 4L /* The number of elements an indexable place holds, and where the first element is. An array is its own storage; a slice points at someone else's. */ -static void indexable_parts(Lower *L, Slot base, const FeType *t, +void indexable_parts(Lower *L, Slot base, const FeType *t, unsigned *data, unsigned *length, FeNode *n) { if (t && t->kind == FE_TYPE_ARRAY) { @@ -326,7 +225,7 @@ static void indexable_parts(Lower *L, Slot base, const FeType *t, /* ------------------------------------------------------- error codes ----- */ -static void note_error_name(Lower *L, const char *name) +void note_error_name(Lower *L, const char *name) { unsigned i; unsigned at; @@ -343,7 +242,7 @@ static void note_error_name(Lower *L, const char *name) ++L->error_count; } -static void collect_error_names(Lower *L, FeNode *n) +void collect_error_names(Lower *L, FeNode *n) { FeNode *x; if (!n) return; @@ -364,7 +263,7 @@ static void collect_error_names(Lower *L, FeNode *n) for (x = n->children; x; x = x->next) collect_error_names(L, x); } -static long error_code(Lower *L, const char *name) +long error_code(Lower *L, const char *name) { unsigned i; for (i = 0; i < L->error_count; ++i) @@ -374,7 +273,7 @@ static long error_code(Lower *L, const char *name) /* ---------------------------------------------------------- expressions --- */ -static FeIrOp binary_op(const char *op, int *is_cmp) +FeIrOp binary_op(const char *op, int *is_cmp) { *is_cmp = 0; if (!op) return FE_IR_ADD; @@ -399,7 +298,7 @@ static FeIrOp binary_op(const char *op, int *is_cmp) return FE_IR_ADD; } -static long literal_value(FeNode *n) +long literal_value(FeNode *n) { const char *s = n->text; long v = 0; @@ -443,7 +342,7 @@ static long literal_value(FeNode *n) /* `and` and `or` do not evaluate the right side unless they have to, so they are control flow rather than an operation. */ -static Slot lower_logical(Lower *L, FeNode *n, int is_and) +Slot lower_logical(Lower *L, FeNode *n, int is_and) { unsigned result = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "logical"); FeIrBlock *rhs = new_block(L); @@ -467,7 +366,7 @@ static Slot lower_logical(Lower *L, FeNode *n, int is_and) /* The builtins that are not calls at all: they are a constant, or they stop the program. `@print` is expanded separately because it becomes several calls rather than one thing. */ -static int lower_builtin(Lower *L, FeNode *n, Slot *out) +int lower_builtin(Lower *L, FeNode *n, Slot *out) { const char *name = n->text; if (!name || name[0] != '@') return 0; @@ -510,7 +409,7 @@ static int lower_builtin(Lower *L, FeNode *n, Slot *out) static const char *RT_ALLOC = "fe_rt_alloc"; static const char *RT_FREE = "fe_rt_free"; -static int is_mem_call(const FeNode *n, const char *what) +int is_mem_call(const FeNode *n, const char *what) { return n && n->a && n->a->kind == FE_N_MEMBER && n->a->a && n->a->a->kind == FE_N_IDENT && n->a->a->text && @@ -520,7 +419,7 @@ static int is_mem_call(const FeNode *n, const char *what) /* Build `!^T`: zero and the pointer when the allocation worked, the out-of-memory code when it did not. */ -static Slot allocation_result(Lower *L, FeNode *n, unsigned pointer) +Slot allocation_result(Lower *L, FeNode *n, unsigned pointer) { FeType *t = n->sem_type; unsigned local = scratch(L, t, "allocated"); @@ -550,7 +449,7 @@ static Slot allocation_result(Lower *L, FeNode *n, unsigned pointer) return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); } -static int lower_mem(Lower *L, FeNode *n, Slot *out) +int lower_mem(Lower *L, FeNode *n, Slot *out) { unsigned args[2]; if (is_mem_call(n, "create")) { @@ -658,1103 +557,11 @@ static int lower_mem(Lower *L, FeNode *n, Slot *out) return 0; } -static Slot lower_call(Lower *L, FeNode *n) -{ - unsigned args[16]; - unsigned count = 0; - FeNode *arg = n->children; - FeType *ret = n->sem_type; - FeIrType rt = ir_type(ret); - unsigned result_local = 0; - const char *callee = n->a && n->a->cname ? n->a->cname : - (n->sem_decl && n->sem_decl->cname ? - n->sem_decl->cname : 0); - { - Slot built; - if (lower_builtin(L, n, &built)) return built; - if (lower_mem(L, n, &built)) return built; - } - if (!callee) { fail(L, "a call with no target", n); return slot_void(); } - /* An aggregate result is written through a hidden first argument. */ - if (rt == FE_IR_MEM) { - result_local = fe_ir_local(L->m, L->fn, FE_IR_MEM, ir_size(ret), - ir_align(ret), "result"); - args[count++] = fe_ir_addr(L->m, L->b, fe_ir_at_local(result_local, 0)); - } - /* A method call passes what it was reached through as its first argument. - `self: Self` and `self: &Self` are the same thing here: the address of - the receiver, because an aggregate never travels in a register. */ - if (n->a && n->a->kind == FE_N_MEMBER && n->sem_decl) { - FeNode *first = n->sem_decl->a ? n->sem_decl->a->children : 0; - if (first && first->text && !strcmp(first->text, "self")) { - FeType *rt = n->a->a ? n->a->a->sem_type : 0; - Slot recv = lower_expr(L, n->a->a); - /* A receiver that is already a reference or an owner is a pointer - already; taking its address would pass a pointer to the - pointer. */ - if (rt && (rt->kind == FE_TYPE_REF || - (rt->kind == FE_TYPE_OWNED && ir_type(rt) == FE_IR_PTR))) - args[count++] = as_value(L, recv, n->a->a); - else - args[count++] = recv.is_place ? as_address(L, recv, n->a->a) - : recv.temp; - } - } - /* A generic call passes its type arguments first. They were consumed when - the instance was chosen and carry no value, so they are not passed. */ - { - FeNode *p; - for (p = n->sem_decl && n->sem_decl->a ? n->sem_decl->a->children : 0; - p && arg; p = p->next) { - if (!(p->flags & FE_NODE_COMPTIME)) break; - arg = arg->next; - } - } - for (; arg; arg = arg->next) { - Slot a = lower_expr(L, arg); - if (count >= 16) { fail(L, "too many arguments", n); break; } - args[count++] = a.type == FE_IR_MEM ? as_address(L, a, arg) - : as_value(L, a, arg); - } - if (rt == FE_IR_MEM) { - fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); - return slot_place(fe_ir_at_local(result_local, 0), FE_IR_MEM, - ir_size(ret)); - } - if (rt == FE_IR_VOID) { - fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); - return slot_void(); - } - return slot_value(fe_ir_call(L->m, L->b, rt, callee, args, count), rt); -} - -static Slot lower_expr_core(Lower *L, FeNode *n); - -/* Every expression may be standing where a wrapper is expected, so the wrap is - applied once, here, rather than at each place that could need it. */ -static Slot lower_expr(Lower *L, FeNode *n) -{ - Slot v; - if (!n || L->failed) return slot_void(); - v = lower_expr_core(L, n); - /* The checker marked the uses that hand ownership away. Where one names a - local we track, the value is no longer ours to release. */ - if ((n->flags & FE_OWN_NODE_CONSUMED) && n->kind == FE_N_IDENT) { - LowerVar *var = find_var(L, n->cname); - unsigned flag; - if (var && release_flag(L, var->local, &flag)) { - unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); - fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); - } - } - return n->sem_context ? wrap_context(L, v, n) : v; -} - -static Slot lower_expr_core(Lower *L, FeNode *n) -{ - FeType *t; - FeIrType it; - if (!n || L->failed) return slot_void(); - t = n->sem_type; - it = ir_type(t); - switch (n->kind) { - case FE_N_LITERAL: - if (n->text && n->text[0] == '"') { - /* The bytes live in the image; the value is a pointer to them and - how many there are. The lexer keeps the quotes and the escapes, - so this is where ` -` becomes one byte. */ - char text[1024]; - unsigned long raw = strlen(n->text); - unsigned long len = 0; - unsigned long i; - const char *label; - unsigned local; - unsigned p; - unsigned c; - if (raw >= 2) raw -= 2; - for (i = 0; i < raw && len + 1 < sizeof text; ++i) { - char ch = n->text[1 + i]; - if (ch == 92 && i + 1 < raw) { /* a backslash */ - ++i; - switch (n->text[1 + i]) { - case 'n': ch = 10; break; - case 't': ch = 9; break; - case 'r': ch = 13; break; - case '0': ch = 0; break; - default: ch = n->text[1 + i]; break; - } - } - text[len++] = ch; - } - label = fe_ir_string(L->m, text, len); - if (!label) { fail(L, "a string literal", n); return slot_void(); } - local = scratch(L, t, "text"); - p = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), p, - FE_IR_PTR); - c = fe_ir_const(L->m, L->b, FE_IR_I32, (long)len); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), c, - FE_IR_I32); - return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); - } - return slot_value(fe_ir_const(L->m, L->b, - it == FE_IR_VOID ? FE_IR_I32 : it, - literal_value(n)), - it == FE_IR_VOID ? FE_IR_I32 : it); - case FE_N_IDENT: { - LowerVar *var = find_var(L, n->cname); - if (var) { - if (var->by_address) { - unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, - fe_ir_at_local(var->local, 0)); - return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); - } - return slot_place(fe_ir_at_local(var->local, 0), it, ir_size(t)); - } - if (n->cname) - return slot_place(fe_ir_at_global(n->cname, 0), it, ir_size(t)); - fail(L, "an unresolved name", n); - return slot_void(); - } - case FE_N_BINARY: { - int is_cmp = 0; - FeIrOp op; - unsigned a; - unsigned b; - FeIrType operand; - if (n->text && !strcmp(n->text, "orelse")) return lower_lazy(L, n, 0); - if (n->text && !strcmp(n->text, "catch")) return lower_lazy(L, n, 1); - if (n->text && (!strcmp(n->text, "and") || !strcmp(n->text, "or"))) - return lower_logical(L, n, !strcmp(n->text, "and")); - op = binary_op(n->text, &is_cmp); - operand = ir_type(n->a ? n->a->sem_type : 0); - if (operand == FE_IR_VOID || operand == FE_IR_MEM) operand = FE_IR_I32; - a = as_value(L, lower_expr(L, n->a), n->a); - b = as_value(L, lower_expr(L, n->b), n->b); - return slot_value(fe_ir_binary(L->m, L->b, op, operand, a, b, - type_is_unsigned(n->a ? n->a->sem_type - : 0)), - is_cmp ? FE_IR_I8 : operand); - } - case FE_N_UNARY: - if (n->text && !strcmp(n->text, "try")) return lower_try(L, n); - if (n->text && !strcmp(n->text, "-")) { - unsigned zero = fe_ir_const(L->m, L->b, it, 0); - unsigned v = as_value(L, lower_expr(L, n->a), n->a); - return slot_value(fe_ir_binary(L->m, L->b, FE_IR_SUB, it, zero, v, - 0), it); - } - if (n->text && !strcmp(n->text, "not")) { - unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); - unsigned v = as_value(L, lower_expr(L, n->a), n->a); - return slot_value(fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I8, v, - zero, 0), FE_IR_I8); - } - if (n->text && (!strcmp(n->text, "&") || !strcmp(n->text, "&mut"))) { - Slot inner = lower_expr(L, n->a); - return slot_value(as_address(L, inner, n->a), FE_IR_PTR); - } - fail(L, "this unary operator", n); - return slot_void(); - case FE_N_MEMBER: - /* A payload-free variant used as a value is just its tag. */ - if (t && t->kind == FE_TYPE_ENUM && !enum_has_payload(t) && - n->b && n->b->text) { - FeVariantType *v = fe_type_variant(t, n->b->text); - if (v) - return slot_value(fe_ir_const(L->m, L->b, ir_type(t), - (long)v->tag), ir_type(t)); - } - /* `error.Name` is a member of the open default set: a code, and - nothing to look up. */ - if (n->a && n->a->kind == FE_N_IDENT && n->a->text && - !strcmp(n->a->text, "error") && n->b && n->b->text) - return slot_value(fe_ir_const(L->m, L->b, FE_IR_I16, - error_code(L, n->b->text)), - FE_IR_I16); - /* `.?` is the payload of an optional the checker already proved is - there. */ - if (n->text && !strcmp(n->text, ".?")) { - FeType *bt = n->a ? n->a->sem_type : 0; - return wrapper_payload(L, lower_expr(L, n->a), bt); - } - /* `p.^` reads through a pointer -- except for an owned slice, whose - pointer and length are the value itself, so there is nothing to - step through. */ - if (n->text && !strcmp(n->text, ".^")) { - Slot base = lower_expr(L, n->a); - unsigned p; - if (base.type == FE_IR_MEM) - return slot_place(base.place, it, ir_size(t)); - p = as_value(L, base, n->a); - return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); - } - /* `.n` is how many elements there are, which an array knows at - compile time and a slice carries beside its pointer. */ - if (n->b && n->b->text && !strcmp(n->b->text, "n")) { - FeType *bt = n->a ? n->a->sem_type : 0; - Slot base; - if (bt && bt->kind == FE_TYPE_ARRAY) - return slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, - (long)bt->length), FE_IR_I32); - base = lower_expr(L, n->a); - if (!base.is_place) { fail(L, "a length of a temporary", n); return slot_void(); } - base.place.offset += SLICE_LEN_OFFSET; - return slot_place(base.place, FE_IR_I32, 4); - } - /* A field is a constant offset from the base. */ - { - FeType *base = n->a ? n->a->sem_type : 0; - FeFieldType *field; - Slot b; - if (base && (base->kind == FE_TYPE_REF || - base->kind == FE_TYPE_OWNED)) base = base->elem; - field = fe_type_field(base, n->b && n->b->text ? n->b->text : ""); - if (!field) { fail(L, "an unresolved field", n); return slot_void(); } - b = lower_expr(L, n->a); - if (n->a->sem_type && (n->a->sem_type->kind == FE_TYPE_REF || - n->a->sem_type->kind == FE_TYPE_OWNED)) { - unsigned p = as_value(L, b, n->a); - return slot_place(fe_ir_at_temp(p, (long)field->offset), it, - ir_size(t)); - } - if (!b.is_place) { fail(L, "a field of a temporary", n); return slot_void(); } - b.place.offset += (long)field->offset; - return slot_place(b.place, it, ir_size(t)); - } - case FE_N_INDEX: { - FeType *bt = n->a ? n->a->sem_type : 0; - FeType *elem = bt ? bt->elem : 0; - Slot base; - unsigned data; - unsigned length; - unsigned index; - unsigned scale; - unsigned offset; - unsigned addr; - if (n->flags & FE_NODE_SLICE) return lower_slice(L, n); - base = lower_expr(L, n->a); - indexable_parts(L, base, bt, &data, &length, n); - index = as_value(L, lower_expr(L, n->b), n->b); - if (!L->c->no_checks) { - unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, - index, length, 1); - guard(L, ok, FE_TRAP_BOUNDS, n->loc.line); - } - scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); - offset = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, index, scale, 1); - addr = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, offset, 1); - return slot_place(fe_ir_at_temp(addr, 0), ir_type(elem), ir_size(elem)); - } - case FE_N_ARRAY_INIT: { - unsigned local = scratch(L, t, "array"); - FeType *elem = t ? t->elem : 0; - unsigned long step = ir_size(elem); - long at = 0; - FeNode *x; - for (x = n->children; x; x = x->next) { - Slot v = lower_expr(L, x); - store_into(L, fe_ir_at_local(local, at), v, x, step); - at += (long)step; - } - return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); - } - case FE_N_STRUCT_INIT: { - unsigned local = scratch(L, t, "struct"); - FeNode *f; - for (f = n->children; f; f = f->next) { - FeFieldType *field; - Slot v; - if (f->kind != FE_N_FIELD) continue; - field = fe_type_field(t, f->text); - if (!field) { fail(L, "an unresolved field", f); return slot_void(); } - v = lower_expr(L, f->a); - store_into(L, fe_ir_at_local(local, (long)field->offset), v, f, - ir_size(field->type)); - } - return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); - } - case FE_N_CALL: - return lower_call(L, n); - case FE_N_TYPE: - /* `x as T`: the operand is `a` and the target type is the node's own. - Between integers this only changes how wide the value is and whether - the top bits repeat the sign. */ - if (n->a) { - FeType *from = n->a->sem_type; - unsigned v = as_value(L, lower_expr(L, n->a), n->a); - if (ir_type(from) == it) return slot_value(v, it); - return slot_value(fe_ir_cast(L->m, L->b, ir_type(from), it, v, - type_is_unsigned(from)), it); - } - fail(L, "this type expression", n); - return slot_void(); - case FE_N_EXPR: - return lower_expr(L, n->a); - default: - fail(L, "this expression", n); - return slot_void(); - } -} - -/* The link name of the `drop` method for this type, found through the instance - the checker recorded. */ -static const char *drop_name(Lower *L, const FeType *t) -{ - unsigned i; - FeNode *method = 0; - if (!t || !t->decl_node) return 0; - for (method = t->decl_node->children; method; method = method->next) - if (method->kind == FE_N_FN && method->text && - !strcmp(method->text, "drop")) break; - if (!method) return 0; - for (i = 0; i < L->c->instance_count; ++i) - if (L->c->instances[i].decl == method && - L->c->instances[i].owner == t) - return L->c->instances[i].cname; - return method->cname; -} - -/* Settle what a scope owes, most recent first. A `return` in the middle of a - function still owes everything, so every exit path calls this. */ -static void run_deferred(Lower *L, unsigned from) -{ - unsigned i; - for (i = L->owed_count; i > from; --i) { - if (L->owed[i - 1].block) { - lower_stmt(L, L->owed[i - 1].block); - continue; - } - { - /* Release only where the value is still here. */ - unsigned live = fe_ir_load(L->m, L->b, FE_IR_I8, - fe_ir_at_local(L->owed[i - 1].flag, 0)); - FeIrBlock *doit = new_block(L); - FeIrBlock *skip = new_block(L); - unsigned args[1]; - FeType *t = L->owed[i - 1].type; - fe_ir_br(L->b, live, doit->id, skip->id); - L->b = doit; - if (t && t->kind == FE_TYPE_OWNED && t->elem && - t->elem->kind == FE_TYPE_SLICE) { - FeIrPlace at = fe_ir_at_local(L->owed[i - 1].local, - SLICE_PTR_OFFSET); - args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); - } else { - args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, - fe_ir_at_local(L->owed[i - 1].local, 0)); - } - if (t && t->has_drop) { - /* A type that says how to let go of itself is asked to; the - name is the one its instance was given. */ - const char *how = drop_name(L, t); - args[0] = fe_ir_addr(L->m, L->b, - fe_ir_at_local(L->owed[i - 1].local, 0)); - if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1); - } else { - fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); - } - fe_ir_jmp(L->b, skip->id); - L->b = skip; - } - } -} - -/* ------------------------------------------------------- wrappers -------- * - * An optional is a tag and a payload; an error union is an error code and a - * payload, where a code of zero means there is no error. Both are memory, and - * both are built the same way: write the tag, then write the value after it. +/* ------------------------------------------------------------ printing --- * + * SPEC 6.3.1: the formatting builtins are not variadic functions. A call is + * expanded here into one write per literal chunk and one per value, so the + * language never grows a variadic calling convention and the format string is + * gone by the time anything runs. * -------------------------------------------------------------------------- */ -static Slot wrap_context(Lower *L, Slot v, FeNode *n) -{ - FeType *want = n->sem_context; - unsigned local; - long payload_at; - if (!want) return v; - local = scratch(L, want, "wrapped"); - payload_at = (long)fe_type_payload_offset(want); - if (want->kind == FE_TYPE_OPTIONAL) { - if (fe_m7_is_null(n)) { - /* A payload with a spare representation uses it for "nothing" - instead of carrying a separate tag. */ - unsigned z = fe_ir_const(L->m, L->b, - uses_niche(want) ? FE_IR_PTR : FE_IR_I8, 0); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), z, - uses_niche(want) ? FE_IR_PTR : FE_IR_I8); - return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); - } - if (!uses_niche(want)) { - unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), one, FE_IR_I8); - } - store_into(L, fe_ir_at_local(local, payload_at), v, n, - ir_size(want->elem)); - return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); - } - if (want->kind == FE_TYPE_ERROR_UNION) { - FeType *value_type = want->error_value; - if (n->sem_type && n->sem_type->is_error) { - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), - as_value(L, v, n), FE_IR_I16); - } else { - unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), zero, FE_IR_I16); - if (value_type && value_type->kind != FE_TYPE_VOID) - store_into(L, fe_ir_at_local(local, payload_at), v, n, - ir_size(value_type)); - } - return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); - } - return v; -} - -/* The tag of a wrapper that is already in memory. */ -static unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n) -{ - FeIrPlace p; - if (!w.is_place) { fail(L, "a wrapper with no place", n); return 0; } - p = w.place; - if (uses_niche(t)) return fe_ir_load(L->m, L->b, FE_IR_PTR, p); - return fe_ir_load(L->m, L->b, tag_type(t), p); -} - -static Slot wrapper_payload(Lower *L, Slot w, const FeType *t) -{ - FeType *payload = t ? (t->kind == FE_TYPE_ERROR_UNION ? t->error_value - : t->elem) : 0; - FeIrPlace p = w.place; - (void)L; - p.offset += (long)fe_type_payload_offset(t); - return slot_place(p, ir_type(payload), ir_size(payload)); -} - -/* Leave the function with this error code, after the deferred blocks. */ -static void return_error(Lower *L, unsigned err, FeNode *n) -{ - FeType *ret = L->ret_type; - unsigned local = scratch(L, ret, "failure"); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), err, FE_IR_I16); - run_deferred(L, 0); - if (L->fn->returns_by_address) { - unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, - fe_ir_at_local(L->ret_local, 0)); - fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), fe_ir_at_local(local, 0), - ir_size(ret)); - fe_ir_ret(L->b, 0, 0); - return; - } - fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(ret), - fe_ir_at_local(local, 0)), 1); - (void)n; -} - -/* `try e` -- if e failed, leave with its error; otherwise the value. */ -static Slot lower_try(Lower *L, FeNode *n) -{ - FeType *t = n->a ? n->a->sem_type : 0; - Slot e = lower_expr(L, n->a); - unsigned err = wrapper_tag(L, e, t, n); - unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); - unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I16, err, zero, 1); - FeIrBlock *bad = new_block(L); - FeIrBlock *good = new_block(L); - fe_ir_br(L->b, ok, good->id, bad->id); - L->b = bad; - return_error(L, err, n); - L->b = good; - return wrapper_payload(L, e, t); -} - -/* `e orelse d` and `e catch d` both mean "the value, or that instead". The - right-hand side is only evaluated when it is needed, so it is a branch. */ -static Slot lower_lazy(Lower *L, FeNode *n, int is_catch) -{ - FeType *t = n->a ? n->a->sem_type : 0; - FeType *payload = t ? (is_catch ? t->error_value : t->elem) : 0; - Slot e; - unsigned tag; - unsigned zero; - unsigned ok; - unsigned result; - FeIrBlock *other; - FeIrBlock *join; - FeIrBlock *have; - e = lower_expr(L, n->a); - tag = wrapper_tag(L, e, t, n); - zero = fe_ir_const(L->m, L->b, is_catch || uses_niche(t) ? FE_IR_PTR - : FE_IR_I8, 0); - /* An error union is fine when its code is zero; an optional is fine when - its tag is not. */ - ok = fe_ir_binary(L->m, L->b, is_catch ? FE_IR_EQ : FE_IR_NE, - is_catch ? FE_IR_I16 : (uses_niche(t) ? FE_IR_PTR - : FE_IR_I8), - tag, zero, 1); - result = scratch(L, payload, "result"); - have = new_block(L); - other = new_block(L); - join = new_block(L); - fe_ir_br(L->b, ok, have->id, other->id); - L->b = have; - store_into(L, fe_ir_at_local(result, 0), wrapper_payload(L, e, t), n, - ir_size(payload)); - fe_ir_jmp(L->b, join->id); - L->b = other; - if (is_catch && n->c) { - /* The block form handles the error and must not fall through with a - value, so whatever it leaves behind is what the checker allowed. */ - lower_stmt(L, n->c); - } else { - Slot d = lower_expr(L, n->b); - store_into(L, fe_ir_at_local(result, 0), d, n->b, ir_size(payload)); - } - fe_ir_jmp(L->b, join->id); - L->b = join; - return slot_place(fe_ir_at_local(result, 0), ir_type(payload), - ir_size(payload)); -} - -/* ----------------------------------------------------------- statements --- */ - -static void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, - unsigned long size) -{ - if (value.type == FE_IR_MEM) { - if (!value.is_place) { fail(L, "an aggregate value", n); return; } - fe_ir_copy(L->m, L->b, dst, value.place, size); - return; - } - fe_ir_store(L->m, L->b, dst, as_value(L, value, n), value.type); -} - -static void lower_return(Lower *L, FeNode *n) -{ - Slot v; - if (!n->a) { - /* A bare return from a `!void` function still has to say that nothing - went wrong. */ - if (L->ret_type && L->ret_type->kind == FE_TYPE_ERROR_UNION) { - unsigned local = scratch(L, L->ret_type, "success"); - unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); - run_deferred(L, 0); - if (L->fn->returns_by_address) { - unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, - fe_ir_at_local(L->ret_local, 0)); - fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), - fe_ir_at_local(local, 0), ir_size(L->ret_type)); - fe_ir_ret(L->b, 0, 0); - return; - } - fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(L->ret_type), - fe_ir_at_local(local, 0)), 1); - return; - } - run_deferred(L, 0); - fe_ir_ret(L->b, 0, 0); - return; - } - /* The value is computed before the deferred blocks run, because they may - destroy what it was read from. */ - v = lower_expr(L, n->a); - if (v.type != FE_IR_MEM && v.is_place) - v = slot_value(as_value(L, v, n->a), v.type); - run_deferred(L, 0); - if (L->fn->returns_by_address) { - unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, - fe_ir_at_local(L->ret_local, 0)); - store_into(L, fe_ir_at_temp(dst, 0), v, n, ir_size(L->ret_type)); - fe_ir_ret(L->b, 0, 0); - return; - } - fe_ir_ret(L->b, as_value(L, v, n->a), 1); -} - -static void lower_if(Lower *L, FeNode *n) -{ - FeIrBlock *then_b = new_block(L); - FeIrBlock *else_b = n->c ? new_block(L) : 0; - FeIrBlock *join = new_block(L); - unsigned cond = as_value(L, lower_expr(L, n->a), n->a); - fe_ir_br(L->b, cond, then_b->id, else_b ? else_b->id : join->id); - L->b = then_b; - lower_stmt(L, n->b); - fe_ir_jmp(L->b, join->id); - if (else_b) { - L->b = else_b; - lower_stmt(L, n->c); - fe_ir_jmp(L->b, join->id); - } - L->b = join; -} - -static void lower_while(Lower *L, FeNode *n) -{ - FeIrBlock *head = new_block(L); - FeIrBlock *body = new_block(L); - FeIrBlock *done = new_block(L); - unsigned cond; - fe_ir_jmp(L->b, head->id); - L->b = head; - cond = as_value(L, lower_expr(L, n->a), n->a); - fe_ir_br(L->b, cond, body->id, done->id); - if (L->loop_depth < 32) { - L->break_target[L->loop_depth] = done->id; - L->continue_target[L->loop_depth] = head->id; - ++L->loop_depth; - } - L->b = body; - lower_stmt(L, n->b); - fe_ir_jmp(L->b, head->id); - if (L->loop_depth) --L->loop_depth; - L->b = done; -} - -/* `x[a..b]` makes a pointer and a length out of part of something indexable. - Both ends are checked -- against each other and against what is there -- - before the pointer is formed. An empty slice of a valid range is fine; one - that starts past its end is not. */ -static Slot lower_slice(Lower *L, FeNode *n) -{ - FeType *bt = n->a ? n->a->sem_type : 0; - FeType *elem = bt ? bt->elem : 0; - FeType *t = n->sem_type; - Slot base = lower_expr(L, n->a); - unsigned data; - unsigned length; - unsigned from; - unsigned to; - unsigned local; - unsigned scale; - unsigned off; - unsigned at; - unsigned count; - indexable_parts(L, base, bt, &data, &length, n); - from = n->b ? as_value(L, lower_expr(L, n->b), n->b) - : fe_ir_const(L->m, L->b, FE_IR_I32, 0); - to = n->c ? as_value(L, lower_expr(L, n->c), n->c) : length; - if (!L->c->no_checks) { - unsigned ordered = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, - from, to, 1); - unsigned within; - guard(L, ordered, FE_TRAP_BOUNDS, n->loc.line); - within = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, to, length, 1); - guard(L, within, FE_TRAP_BOUNDS, n->loc.line); - } - scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); - off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, from, scale, 1); - at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, off, 1); - count = fe_ir_binary(L->m, L->b, FE_IR_SUB, FE_IR_I32, to, from, 1); - local = scratch(L, t, "slice"); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), at, - FE_IR_PTR); - fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), count, - FE_IR_I32); - return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); -} - -/* Three shapes share the keyword. - - for i in a..b { } counts - for x in thing { } walks, binding a reference to each element - for i, x in thing { } walks, binding the position as well - - The count is read once before the body, so a thing that grows underneath the - loop cannot walk past what was measured. The element binding is a reference - (`x.^` reads it), which is what lets a loop write back into the thing. */ -static void lower_for(Lower *L, FeNode *n) -{ - FeIrBlock *head; - FeIrBlock *body; - FeIrBlock *step; - FeIrBlock *done; - unsigned counter; - unsigned limit; - - if (n->c) { - /* The counting form: the variable is the count itself. */ - unsigned from = as_value(L, lower_expr(L, n->a), n->a); - unsigned to; - counter = declare_var(L, n->cname, 0, n->text); - L->fn->locals[counter].type = FE_IR_I32; - L->fn->locals[counter].size = 4; - L->fn->locals[counter].align = 4; - fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), from, FE_IR_I32); - to = as_value(L, lower_expr(L, n->c), n->c); - limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "limit"); - fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), to, FE_IR_I32); - head = new_block(L); - body = new_block(L); - step = new_block(L); - done = new_block(L); - fe_ir_jmp(L->b, head->id); - L->b = head; - { - unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, - fe_ir_at_local(counter, 0)); - unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, - fe_ir_at_local(limit, 0)); - unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); - fe_ir_br(L->b, more, body->id, done->id); - } - } else { - FeType *bt = n->a ? n->a->sem_type : 0; - FeType *elem = bt ? bt->elem : 0; - Slot base = lower_expr(L, n->a); - unsigned data; - unsigned length; - unsigned data_local; - unsigned item; - indexable_parts(L, base, bt, &data, &length, n); - data_local = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, "data"); - fe_ir_store(L->m, L->b, fe_ir_at_local(data_local, 0), data, FE_IR_PTR); - limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "count"); - fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), length, FE_IR_I32); - /* With two names the first is the position and the second the element; - with one it is the element. */ - counter = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "index"); - if (n->aux_cname) { - L->vars[L->var_count].cname = n->cname; - L->vars[L->var_count].local = counter; - L->vars[L->var_count].by_address = 0; - if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; - item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->aux_text); - L->vars[L->var_count].cname = n->aux_cname; - L->vars[L->var_count].local = item; - L->vars[L->var_count].by_address = 0; - if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; - } else { - item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->text); - L->vars[L->var_count].cname = n->cname; - L->vars[L->var_count].local = item; - L->vars[L->var_count].by_address = 0; - if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; - } - { - unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I32, 0); - fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), zero, FE_IR_I32); - } - head = new_block(L); - body = new_block(L); - step = new_block(L); - done = new_block(L); - fe_ir_jmp(L->b, head->id); - L->b = head; - { - unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, - fe_ir_at_local(counter, 0)); - unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, - fe_ir_at_local(limit, 0)); - unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); - fe_ir_br(L->b, more, body->id, done->id); - } - L->b = body; - { - unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, - fe_ir_at_local(counter, 0)); - unsigned scale = fe_ir_const(L->m, L->b, FE_IR_I32, - (long)ir_size(elem)); - unsigned off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, i, - scale, 1); - unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, - fe_ir_at_local(data_local, 0)); - unsigned at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, p, - off, 1); - fe_ir_store(L->m, L->b, fe_ir_at_local(item, 0), at, FE_IR_PTR); - } - L->b = head; - } - - if (L->loop_depth < 32) { - L->break_target[L->loop_depth] = done->id; - L->continue_target[L->loop_depth] = step->id; - ++L->loop_depth; - } - L->b = body; - lower_stmt(L, n->b); - fe_ir_jmp(L->b, step->id); - L->b = step; - { - unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, - fe_ir_at_local(counter, 0)); - unsigned one = fe_ir_const(L->m, L->b, FE_IR_I32, 1); - unsigned next = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_I32, i, one, 1); - fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), next, FE_IR_I32); - } - fe_ir_jmp(L->b, head->id); - if (L->loop_depth) --L->loop_depth; - L->b = done; -} - -/* `match` over a payload-free enum or an integer: compare the tag against each - arm's pattern in turn. The checker already proved the arms cover everything, - so falling off the end cannot happen in a program that compiled -- but the - generated code has to go somewhere, and going to the join is right. */ -static void lower_match(Lower *L, FeNode *n) -{ - FeType *t = n->a ? n->a->sem_type : 0; - FeIrType it = ir_type(t); - Slot subject = lower_expr(L, n->a); - unsigned value; - FeIrBlock *join; - FeNode *arm; - if (it == FE_IR_MEM) { fail(L, "a match over a payload", n); return; } - value = as_value(L, subject, n->a); - join = new_block(L); - for (arm = n->children; arm; arm = arm->next) { - FeIrBlock *body; - FeIrBlock *next; - unsigned want; - unsigned same; - FeVariantType *v; - if (arm->kind != FE_N_ARM) continue; - if (arm->text && !strcmp(arm->text, "_")) { - lower_stmt(L, arm->a); - fe_ir_jmp(L->b, join->id); - L->b = join; - return; - } - v = t && t->kind == FE_TYPE_ENUM && arm->text - ? fe_type_variant(t, arm->text) : 0; - want = fe_ir_const(L->m, L->b, it, - v ? (long)v->tag : literal_value(arm)); - same = fe_ir_binary(L->m, L->b, FE_IR_EQ, it, value, want, 1); - body = new_block(L); - next = new_block(L); - fe_ir_br(L->b, same, body->id, next->id); - L->b = body; - lower_stmt(L, arm->a); - fe_ir_jmp(L->b, join->id); - L->b = next; - } - fe_ir_jmp(L->b, join->id); - L->b = join; -} - -static void lower_stmt(Lower *L, FeNode *n) -{ - FeNode *x; - if (!n || L->failed) return; - switch (n->kind) { - case FE_N_BLOCK: { - unsigned outer = L->owed_count; - for (x = n->children; x; x = x->next) lower_stmt(L, x); - /* Leaving a block normally settles what it owes. An exit that jumped - away already settled on its way out. */ - if (!L->b->terminated) run_deferred(L, outer); - L->owed_count = outer; - return; - } - case FE_N_LET: - case FE_N_VAR: - case FE_N_CONST: { - unsigned local = declare_var(L, n->cname, n->sem_type, n->text); - if (n->b) { - Slot v = lower_expr(L, n->b); - unsigned flag; - store_into(L, fe_ir_at_local(local, 0), v, n, ir_size(n->sem_type)); - if (release_flag(L, local, &flag)) { - unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); - fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), one, FE_IR_I8); - } - } - return; - } - case FE_N_ASSIGN: { - Slot dst = lower_expr(L, n->a); - Slot v = lower_expr(L, n->b); - if (!dst.is_place) { fail(L, "an assignment to a value", n); return; } - store_into(L, dst.place, v, n, dst.size); - return; - } - case FE_N_EXPR_STMT: - lower_expr(L, n->a); - return; - case FE_N_RETURN: - lower_return(L, n); - return; - case FE_N_IF: - lower_if(L, n); - return; - case FE_N_WHILE: - lower_while(L, n); - return; - case FE_N_BREAK: - if (L->loop_depth) fe_ir_jmp(L->b, L->break_target[L->loop_depth - 1]); - return; - case FE_N_CONTINUE: - if (L->loop_depth) - fe_ir_jmp(L->b, L->continue_target[L->loop_depth - 1]); - return; - case FE_N_UNSAFE: - lower_stmt(L, n->a); - return; - case FE_N_DEFER: - if (L->owed_count < 64) { - L->owed[L->owed_count].block = n->a; - L->owed[L->owed_count].local = 0; - L->owed[L->owed_count].flag = 0; - L->owed[L->owed_count].type = 0; - ++L->owed_count; - } - return; - case FE_N_FOR: - lower_for(L, n); - return; - case FE_N_MATCH: - lower_match(L, n); - return; - default: - fail(L, "this statement", n); - return; - } -} - -/* ------------------------------------------------------------ functions --- */ - -/* A global is static storage. SPEC 7.1: its initializer is evaluated at - compile time, so what reaches here is either a constant to place in the - image or nothing, and the storage starts as zeroes. */ -static void lower_global(Lower *L, FeNode *n) -{ - FeType *t = n->sem_type; - unsigned char *init = 0; - unsigned long size = ir_size(t); - if (!n->cname) return; - if (n->b && n->b->kind == FE_N_LITERAL && size && size <= 8) { - long v = literal_value(n->b); - unsigned long i; - init = (unsigned char *)fe_arena_alloc(&L->m->arena, (size_t)size); - if (init) - for (i = 0; i < size; ++i) - init[i] = (unsigned char)((v >> (i * 8)) & 0xFF); - } - fe_ir_global(L->m, n->cname, ir_type(t), size, ir_align(t), init); -} - -static int fn_is_generic(const FeNode *fn) -{ - FeNode *p; - if (!fn) return 0; - for (p = fn->a ? fn->a->children : 0; p; p = p->next) - if (p->flags & FE_NODE_COMPTIME) return 1; - return 0; -} - -static void lower_fn_as(Lower *L, FeNode *fn, const char *name) -{ - FeNode *p; - FeType *ret = fn->b ? fe_type_from_ast(&L->c->types, fn->b) : 0; - FeIrFunc *f; - if (!name) return; - f = fe_ir_func(L->m, name, ir_type(ret), ir_size(ret)); - if (!f) return; - L->fn = f; - L->ret_type = ret; - L->var_count = 0; - L->loop_depth = 0; - /* A hidden first parameter holds where an aggregate result goes. */ - if (f->returns_by_address) - L->ret_local = fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, "result"); - for (p = fn->a ? fn->a->children : 0; p; p = p->next) { - FeType *pt; - int by_address; - unsigned local; - /* A comptime parameter was consumed at compile time; it has no - storage and takes no argument slot. */ - if (p->flags & FE_NODE_COMPTIME) continue; - pt = fe_type_from_ast(&L->c->types, p->a); - /* An aggregate parameter arrives as an address. */ - by_address = ir_type(pt) == FE_IR_MEM; - local = by_address - ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) - : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), - p->text); - if (L->var_count < LOWER_MAX_LOCALS) { - L->vars[L->var_count].cname = p->cname; - L->vars[L->var_count].local = local; - L->vars[L->var_count].by_address = by_address; - ++L->var_count; - } - } - f->param_count = f->local_count; - L->b = fe_ir_block(L->m, f); - lower_stmt(L, fn->c); - /* A void function may just run off the end. */ - fe_ir_ret(L->b, 0, 0); -} - -static void lower_fn(Lower *L, FeNode *fn) -{ - lower_fn_as(L, fn, fn->cname); -} - -int fe_lower_program(FeCheck *c, FeIrModule *out) -{ - Lower L; - unsigned u; - FeNode *n; - memset(&L, 0, sizeof L); - L.c = c; - L.m = out; - /* The codes have to be known while the bodies are lowered, so the names - are gathered from the whole build first. */ - for (u = 0; u < c->build->count; ++u) - collect_error_names(&L, c->build->units[u].ast.root); - for (u = 0; u < c->build->count; ++u) { - FeUnit *unit = &c->build->units[u]; - c->ast = &unit->ast; - c->unit = unit; - c->types.unit_name = unit->name[0] ? unit->name : "unit"; - if (!out->unit_file || !out->unit_file[0]) out->unit_file = unit->path; - for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) - if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) - lower_global(&L, n); - else if (n->kind == FE_N_FN && !n->c) { - /* A declaration with no body is something the linker will - find: the runtime, or a C library. */ - FeType *ret = n->b ? fe_type_from_ast(&c->types, n->b) : 0; - FeIrFunc *f; - if (!n->cname) continue; - f = fe_ir_func(out, n->cname, ir_type(ret), ir_size(ret)); - if (f) f->is_extern = 1; - } - else if (n->kind == FE_N_FN && n->c && !fn_is_generic(n)) { - lower_fn(&L, n); - /* The entry unit is the one the build was rooted at. */ - if (u == 0 && n->text && !strcmp(n->text, "main")) - out->entry_main = n->cname; - } - } - /* Each instance the checker reached is a function of its own: the same - body, read with different types bound, under its own link name. This is - where monomorphisation actually produces code -- the front end only - decided which instances exist. */ - for (u = 0; u < c->instance_count && !L.failed; ++u) { - FeInstance *inst = &c->instances[u]; - FeUnit *home; - FeTypeBind save[FE_TYPE_PARAM_MAX]; - unsigned save_count; - unsigned k; - if (!inst->decl || !inst->decl->c || !inst->cname || !inst->home) - continue; - home = 0; - for (k = 0; k < c->build->count; ++k) - if (!strcmp(c->build->units[k].name, inst->home)) - home = &c->build->units[k]; - if (!home) continue; - c->ast = &home->ast; - c->unit = home; - c->types.unit_name = home->name; - save_count = c->types.param_count; - for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) save[k] = c->types.params[k]; - c->types.param_count = inst->bind_count; - for (k = 0; k < inst->bind_count && k < FE_TYPE_PARAM_MAX; ++k) - c->types.params[k] = inst->binds[k]; - lower_fn_as(&L, inst->decl, inst->cname); - c->types.param_count = save_count; - for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) c->types.params[k] = save[k]; - } - return !L.failed; -} +/* Write `len` bytes of a literal that is already in the image. */ diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c new file mode 100644 index 0000000..e43cfc5 --- /dev/null +++ b/fec/src/lowerexp.c @@ -0,0 +1,468 @@ +#include "lowerpri.h" + +Slot lower_expr_core(Lower *L, FeNode *n) +{ + FeType *t; + FeIrType it; + if (!n || L->failed) return slot_void(); + t = n->sem_type; + it = ir_type(t); + switch (n->kind) { + case FE_N_LITERAL: + if (n->text && n->text[0] == '"') { + /* The bytes live in the image; the value is a pointer to them and + how many there are. The lexer keeps the quotes and the escapes, + so this is where ` +` becomes one byte. */ + char text[1024]; + unsigned long raw = strlen(n->text); + unsigned long len = 0; + unsigned long i; + const char *label; + unsigned local; + unsigned p; + unsigned c; + if (raw >= 2) raw -= 2; + for (i = 0; i < raw && len + 1 < sizeof text; ++i) { + char ch = n->text[1 + i]; + if (ch == 92 && i + 1 < raw) { /* a backslash */ + ++i; + switch (n->text[1 + i]) { + case 'n': ch = 10; break; + case 't': ch = 9; break; + case 'r': ch = 13; break; + case '0': ch = 0; break; + default: ch = n->text[1 + i]; break; + } + } + text[len++] = ch; + } + label = fe_ir_string(L->m, text, len); + if (!label) { fail(L, "a string literal", n); return slot_void(); } + local = scratch(L, t, "text"); + p = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), p, + FE_IR_PTR); + c = fe_ir_const(L->m, L->b, FE_IR_I32, (long)len); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), c, + FE_IR_I32); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + return slot_value(fe_ir_const(L->m, L->b, + it == FE_IR_VOID ? FE_IR_I32 : it, + literal_value(n)), + it == FE_IR_VOID ? FE_IR_I32 : it); + case FE_N_IDENT: { + LowerVar *var = find_var(L, n->cname); + if (var) { + if (var->by_address) { + unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(var->local, 0)); + return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); + } + return slot_place(fe_ir_at_local(var->local, 0), it, ir_size(t)); + } + if (n->cname) + return slot_place(fe_ir_at_global(n->cname, 0), it, ir_size(t)); + fail(L, "an unresolved name", n); + return slot_void(); + } + case FE_N_BINARY: { + int is_cmp = 0; + FeIrOp op; + unsigned a; + unsigned b; + FeIrType operand; + if (n->text && !strcmp(n->text, "orelse")) return lower_lazy(L, n, 0); + if (n->text && !strcmp(n->text, "catch")) return lower_lazy(L, n, 1); + if (n->text && (!strcmp(n->text, "and") || !strcmp(n->text, "or"))) + return lower_logical(L, n, !strcmp(n->text, "and")); + op = binary_op(n->text, &is_cmp); + operand = ir_type(n->a ? n->a->sem_type : 0); + if (operand == FE_IR_VOID || operand == FE_IR_MEM) operand = FE_IR_I32; + a = as_value(L, lower_expr(L, n->a), n->a); + b = as_value(L, lower_expr(L, n->b), n->b); + return slot_value(fe_ir_binary(L->m, L->b, op, operand, a, b, + type_is_unsigned(n->a ? n->a->sem_type + : 0)), + is_cmp ? FE_IR_I8 : operand); + } + case FE_N_UNARY: + if (n->text && !strcmp(n->text, "try")) return lower_try(L, n); + if (n->text && !strcmp(n->text, "-")) { + unsigned zero = fe_ir_const(L->m, L->b, it, 0); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_SUB, it, zero, v, + 0), it); + } + if (n->text && !strcmp(n->text, "not")) { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I8, v, + zero, 0), FE_IR_I8); + } + if (n->text && (!strcmp(n->text, "&") || !strcmp(n->text, "&mut"))) { + Slot inner = lower_expr(L, n->a); + return slot_value(as_address(L, inner, n->a), FE_IR_PTR); + } + fail(L, "this unary operator", n); + return slot_void(); + case FE_N_MEMBER: + /* A payload-free variant used as a value is just its tag. */ + if (t && t->kind == FE_TYPE_ENUM && !enum_has_payload(t) && + n->b && n->b->text) { + FeVariantType *v = fe_type_variant(t, n->b->text); + if (v) + return slot_value(fe_ir_const(L->m, L->b, ir_type(t), + (long)v->tag), ir_type(t)); + } + /* `error.Name` is a member of the open default set: a code, and + nothing to look up. */ + if (n->a && n->a->kind == FE_N_IDENT && n->a->text && + !strcmp(n->a->text, "error") && n->b && n->b->text) + return slot_value(fe_ir_const(L->m, L->b, FE_IR_I16, + error_code(L, n->b->text)), + FE_IR_I16); + /* `.?` is the payload of an optional the checker already proved is + there. */ + if (n->text && !strcmp(n->text, ".?")) { + FeType *bt = n->a ? n->a->sem_type : 0; + return wrapper_payload(L, lower_expr(L, n->a), bt); + } + /* `p.^` reads through a pointer -- except for an owned slice, whose + pointer and length are the value itself, so there is nothing to + step through. */ + if (n->text && !strcmp(n->text, ".^")) { + Slot base = lower_expr(L, n->a); + unsigned p; + if (base.type == FE_IR_MEM) + return slot_place(base.place, it, ir_size(t)); + p = as_value(L, base, n->a); + return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); + } + /* `.n` is how many elements there are, which an array knows at + compile time and a slice carries beside its pointer. */ + if (n->b && n->b->text && !strcmp(n->b->text, "n")) { + FeType *bt = n->a ? n->a->sem_type : 0; + Slot base; + if (bt && bt->kind == FE_TYPE_ARRAY) + return slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, + (long)bt->length), FE_IR_I32); + base = lower_expr(L, n->a); + if (!base.is_place) { fail(L, "a length of a temporary", n); return slot_void(); } + base.place.offset += SLICE_LEN_OFFSET; + return slot_place(base.place, FE_IR_I32, 4); + } + /* A field is a constant offset from the base. */ + { + FeType *base = n->a ? n->a->sem_type : 0; + FeFieldType *field; + Slot b; + if (base && (base->kind == FE_TYPE_REF || + base->kind == FE_TYPE_OWNED)) base = base->elem; + field = fe_type_field(base, n->b && n->b->text ? n->b->text : ""); + if (!field) { fail(L, "an unresolved field", n); return slot_void(); } + b = lower_expr(L, n->a); + if (n->a->sem_type && (n->a->sem_type->kind == FE_TYPE_REF || + n->a->sem_type->kind == FE_TYPE_OWNED)) { + unsigned p = as_value(L, b, n->a); + return slot_place(fe_ir_at_temp(p, (long)field->offset), it, + ir_size(t)); + } + if (!b.is_place) { fail(L, "a field of a temporary", n); return slot_void(); } + b.place.offset += (long)field->offset; + return slot_place(b.place, it, ir_size(t)); + } + case FE_N_INDEX: { + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + Slot base; + unsigned data; + unsigned length; + unsigned index; + unsigned scale; + unsigned offset; + unsigned addr; + if (n->flags & FE_NODE_SLICE) return lower_slice(L, n); + base = lower_expr(L, n->a); + indexable_parts(L, base, bt, &data, &length, n); + index = as_value(L, lower_expr(L, n->b), n->b); + if (!L->c->no_checks) { + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, + index, length, 1); + guard(L, ok, FE_TRAP_BOUNDS, n->loc.line); + } + scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + offset = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, index, scale, 1); + addr = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, offset, 1); + return slot_place(fe_ir_at_temp(addr, 0), ir_type(elem), ir_size(elem)); + } + case FE_N_ARRAY_INIT: { + unsigned local = scratch(L, t, "array"); + FeType *elem = t ? t->elem : 0; + unsigned long step = ir_size(elem); + long at = 0; + FeNode *x; + for (x = n->children; x; x = x->next) { + Slot v = lower_expr(L, x); + store_into(L, fe_ir_at_local(local, at), v, x, step); + at += (long)step; + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + case FE_N_STRUCT_INIT: { + unsigned local = scratch(L, t, "struct"); + FeNode *f; + for (f = n->children; f; f = f->next) { + FeFieldType *field; + Slot v; + if (f->kind != FE_N_FIELD) continue; + field = fe_type_field(t, f->text); + if (!field) { fail(L, "an unresolved field", f); return slot_void(); } + v = lower_expr(L, f->a); + store_into(L, fe_ir_at_local(local, (long)field->offset), v, f, + ir_size(field->type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + case FE_N_CALL: + return lower_call(L, n); + case FE_N_TYPE: + /* `x as T`: the operand is `a` and the target type is the node's own. + Between integers this only changes how wide the value is and whether + the top bits repeat the sign. */ + if (n->a) { + FeType *from = n->a->sem_type; + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + if (ir_type(from) == it) return slot_value(v, it); + return slot_value(fe_ir_cast(L->m, L->b, ir_type(from), it, v, + type_is_unsigned(from)), it); + } + fail(L, "this type expression", n); + return slot_void(); + case FE_N_EXPR: + return lower_expr(L, n->a); + default: + fail(L, "this expression", n); + return slot_void(); + } +} + +/* The link name of the `drop` method for this type, found through the instance + the checker recorded. */ +const char *drop_name(Lower *L, const FeType *t) +{ + unsigned i; + FeNode *method = 0; + if (!t || !t->decl_node) return 0; + for (method = t->decl_node->children; method; method = method->next) + if (method->kind == FE_N_FN && method->text && + !strcmp(method->text, "drop")) break; + if (!method) return 0; + for (i = 0; i < L->c->instance_count; ++i) + if (L->c->instances[i].decl == method && + L->c->instances[i].owner == t) + return L->c->instances[i].cname; + return method->cname; +} + +/* Settle what a scope owes, most recent first. A `return` in the middle of a + function still owes everything, so every exit path calls this. */ +void run_deferred(Lower *L, unsigned from) +{ + unsigned i; + for (i = L->owed_count; i > from; --i) { + if (L->owed[i - 1].block) { + lower_stmt(L, L->owed[i - 1].block); + continue; + } + { + /* Release only where the value is still here. */ + unsigned live = fe_ir_load(L->m, L->b, FE_IR_I8, + fe_ir_at_local(L->owed[i - 1].flag, 0)); + FeIrBlock *doit = new_block(L); + FeIrBlock *skip = new_block(L); + unsigned args[1]; + FeType *t = L->owed[i - 1].type; + fe_ir_br(L->b, live, doit->id, skip->id); + L->b = doit; + if (t && t->kind == FE_TYPE_OWNED && t->elem && + t->elem->kind == FE_TYPE_SLICE) { + FeIrPlace at = fe_ir_at_local(L->owed[i - 1].local, + SLICE_PTR_OFFSET); + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); + } else { + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->owed[i - 1].local, 0)); + } + if (t && t->has_drop) { + /* A type that says how to let go of itself is asked to; the + name is the one its instance was given. */ + const char *how = drop_name(L, t); + args[0] = fe_ir_addr(L->m, L->b, + fe_ir_at_local(L->owed[i - 1].local, 0)); + if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1); + } else { + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); + } + fe_ir_jmp(L->b, skip->id); + L->b = skip; + } + } +} + +/* ------------------------------------------------------- wrappers -------- * + * An optional is a tag and a payload; an error union is an error code and a + * payload, where a code of zero means there is no error. Both are memory, and + * both are built the same way: write the tag, then write the value after it. + * -------------------------------------------------------------------------- */ + +Slot wrap_context(Lower *L, Slot v, FeNode *n) +{ + FeType *want = n->sem_context; + unsigned local; + long payload_at; + if (!want) return v; + local = scratch(L, want, "wrapped"); + payload_at = (long)fe_type_payload_offset(want); + if (want->kind == FE_TYPE_OPTIONAL) { + if (fe_m7_is_null(n)) { + /* A payload with a spare representation uses it for "nothing" + instead of carrying a separate tag. */ + unsigned z = fe_ir_const(L->m, L->b, + uses_niche(want) ? FE_IR_PTR : FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), z, + uses_niche(want) ? FE_IR_PTR : FE_IR_I8); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + if (!uses_niche(want)) { + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), one, FE_IR_I8); + } + store_into(L, fe_ir_at_local(local, payload_at), v, n, + ir_size(want->elem)); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + if (want->kind == FE_TYPE_ERROR_UNION) { + FeType *value_type = want->error_value; + if (n->sem_type && n->sem_type->is_error) { + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), + as_value(L, v, n), FE_IR_I16); + } else { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), zero, FE_IR_I16); + if (value_type && value_type->kind != FE_TYPE_VOID) + store_into(L, fe_ir_at_local(local, payload_at), v, n, + ir_size(value_type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(want)); + } + return v; +} + +/* The tag of a wrapper that is already in memory. */ +unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n) +{ + FeIrPlace p; + if (!w.is_place) { fail(L, "a wrapper with no place", n); return 0; } + p = w.place; + if (uses_niche(t)) return fe_ir_load(L->m, L->b, FE_IR_PTR, p); + return fe_ir_load(L->m, L->b, tag_type(t), p); +} + +Slot wrapper_payload(Lower *L, Slot w, const FeType *t) +{ + FeType *payload = t ? (t->kind == FE_TYPE_ERROR_UNION ? t->error_value + : t->elem) : 0; + FeIrPlace p = w.place; + (void)L; + p.offset += (long)fe_type_payload_offset(t); + return slot_place(p, ir_type(payload), ir_size(payload)); +} + +/* Leave the function with this error code, after the deferred blocks. */ +void return_error(Lower *L, unsigned err, FeNode *n) +{ + FeType *ret = L->ret_type; + unsigned local = scratch(L, ret, "failure"); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), err, FE_IR_I16); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), fe_ir_at_local(local, 0), + ir_size(ret)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(ret), + fe_ir_at_local(local, 0)), 1); + (void)n; +} + +/* `try e` -- if e failed, leave with its error; otherwise the value. */ +Slot lower_try(Lower *L, FeNode *n) +{ + FeType *t = n->a ? n->a->sem_type : 0; + Slot e = lower_expr(L, n->a); + unsigned err = wrapper_tag(L, e, t, n); + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + unsigned ok = fe_ir_binary(L->m, L->b, FE_IR_EQ, FE_IR_I16, err, zero, 1); + FeIrBlock *bad = new_block(L); + FeIrBlock *good = new_block(L); + fe_ir_br(L->b, ok, good->id, bad->id); + L->b = bad; + return_error(L, err, n); + L->b = good; + return wrapper_payload(L, e, t); +} + +/* `e orelse d` and `e catch d` both mean "the value, or that instead". The + right-hand side is only evaluated when it is needed, so it is a branch. */ +Slot lower_lazy(Lower *L, FeNode *n, int is_catch) +{ + FeType *t = n->a ? n->a->sem_type : 0; + FeType *payload = t ? (is_catch ? t->error_value : t->elem) : 0; + Slot e; + unsigned tag; + unsigned zero; + unsigned ok; + unsigned result; + FeIrBlock *other; + FeIrBlock *join; + FeIrBlock *have; + e = lower_expr(L, n->a); + tag = wrapper_tag(L, e, t, n); + zero = fe_ir_const(L->m, L->b, is_catch || uses_niche(t) ? FE_IR_PTR + : FE_IR_I8, 0); + /* An error union is fine when its code is zero; an optional is fine when + its tag is not. */ + ok = fe_ir_binary(L->m, L->b, is_catch ? FE_IR_EQ : FE_IR_NE, + is_catch ? FE_IR_I16 : (uses_niche(t) ? FE_IR_PTR + : FE_IR_I8), + tag, zero, 1); + result = scratch(L, payload, "result"); + have = new_block(L); + other = new_block(L); + join = new_block(L); + fe_ir_br(L->b, ok, have->id, other->id); + L->b = have; + store_into(L, fe_ir_at_local(result, 0), wrapper_payload(L, e, t), n, + ir_size(payload)); + fe_ir_jmp(L->b, join->id); + L->b = other; + if (is_catch && n->c) { + /* The block form handles the error and must not fall through with a + value, so whatever it leaves behind is what the checker allowed. */ + lower_stmt(L, n->c); + } else { + Slot d = lower_expr(L, n->b); + store_into(L, fe_ir_at_local(result, 0), d, n->b, ir_size(payload)); + } + fe_ir_jmp(L->b, join->id); + L->b = join; + return slot_place(fe_ir_at_local(result, 0), ir_type(payload), + ir_size(payload)); +} + +/* ----------------------------------------------------------- statements --- */ diff --git a/fec/src/lowerpri.h b/fec/src/lowerpri.h new file mode 100644 index 0000000..a72262b --- /dev/null +++ b/fec/src/lowerpri.h @@ -0,0 +1,153 @@ +#ifndef FE_LOWERPRI_H +#define FE_LOWERPRI_H + +/* Lowering's own vocabulary, shared by the files it is split across. */ + +#include "lower.h" +#include "m7.h" +#include "own.h" +#include +#include + +#include +#include "m7.h" +#include "own.h" +#include + +/* ------------------------------------------------------------------------- * + * Lowering + * + * One function at a time, one statement at a time. A `Slot` is what an + * expression produced: either a value already in a temporary, or a place in + * memory that a value can be read from or written to. Aggregates are always + * places -- they are never carried in a temporary, because a temporary is a + * register and an aggregate does not fit in one. + * ------------------------------------------------------------------------- */ + +#define LOWER_MAX_LOCALS 256 + +typedef struct LowerVar { + const char *cname; + unsigned local; + /* An aggregate parameter arrives as an address, so the slot holds a + pointer and the value is one dereference away. */ + int by_address; +} LowerVar; + +typedef struct Lower { + FeCheck *c; + FeIrModule *m; + FeIrFunc *fn; + FeIrBlock *b; /* the block being appended to */ + FeType *ret_type; + unsigned ret_local; /* hidden result address, when returning mem */ + LowerVar vars[LOWER_MAX_LOCALS]; + unsigned var_count; + /* Loop targets, for break and continue. */ + unsigned break_target[32]; + unsigned continue_target[32]; + unsigned loop_depth; + /* What a scope still owes when it ends: `defer` blocks to run and owned + values to release, in the order they were written. Every exit path runs + what is live, last first. + + A drop carries a flag beside the value. The flag is set when the value + is stored and cleared wherever it is moved away, so the release happens + exactly on the paths where the value is still there -- which is not + something the shape of the code can tell you on its own. */ + struct { + FeNode *block; /* a `defer`, when set */ + unsigned local; /* the owned value, otherwise */ + unsigned flag; + FeType *type; + } owed[64]; + unsigned owed_count; + /* Every `error.Name` used anywhere in the build, sorted, numbered from one. + SPEC 4.6: the names are collected rather than declared, and the order is + fixed by the spelling so that the same program always gets the same + codes however the build was ordered. */ + const char *error_names[256]; + unsigned error_count; + int failed; +} Lower; + +typedef struct Slot { + int is_place; + unsigned temp; /* the value, when is_place is 0 */ + FeIrPlace place; /* where it lives, when is_place is 1 */ + FeIrType type; + unsigned long size; /* for FE_IR_MEM */ +} Slot; + + + +/* A slice is a pointer and a length, in that order. Both the compiler and the + runtime read it this way, so the offsets live here and nowhere else. */ +#define SLICE_PTR_OFFSET 0L +#define SLICE_LEN_OFFSET 4L + +/* Every definition in lowering, so the split files can see each other. */ +void fail(Lower *L, const char *why, FeNode *n); +FeIrType ir_type_of(const FeType *t); +int enum_has_payload(const FeType *t); +FeIrType ir_type(const FeType *t); +unsigned long ir_size(const FeType *t); +unsigned ir_align(const FeType *t); +int type_is_unsigned(const FeType *t); +Slot slot_value(unsigned temp, FeIrType t); +Slot slot_place(FeIrPlace p, FeIrType t, unsigned long size); +Slot slot_void(void); +unsigned as_value(Lower *L, Slot s, FeNode *n); +unsigned as_address(Lower *L, Slot s, FeNode *n); +int needs_release(const FeType *t); +unsigned declare_var(Lower *L, const char *cname, const FeType *t, + const char *name); +int release_flag(Lower *L, unsigned local, unsigned *flag); +LowerVar *find_var(Lower *L, const char *cname); +FeIrBlock *new_block(Lower *L); +void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line); +FeIrType tag_type(const FeType *t); +int uses_niche(const FeType *t); +unsigned scratch(Lower *L, const FeType *t, const char *why); +void indexable_parts(Lower *L, Slot base, const FeType *t, + unsigned *data, unsigned *length, FeNode *n); +void note_error_name(Lower *L, const char *name); +void collect_error_names(Lower *L, FeNode *n); +long error_code(Lower *L, const char *name); +FeIrOp binary_op(const char *op, int *is_cmp); +long literal_value(FeNode *n); +Slot lower_logical(Lower *L, FeNode *n, int is_and); +int lower_builtin(Lower *L, FeNode *n, Slot *out); +int is_mem_call(const FeNode *n, const char *what); +Slot allocation_result(Lower *L, FeNode *n, unsigned pointer); +int lower_mem(Lower *L, FeNode *n, Slot *out); +void emit_text(Lower *L, unsigned handle, const char *text, + unsigned long len); +void emit_value_text(Lower *L, unsigned handle, FeNode *arg, int verb); +int lower_print(Lower *L, FeNode *n, Slot *out); +Slot lower_call(Lower *L, FeNode *n); +Slot lower_expr(Lower *L, FeNode *n); +Slot lower_expr_core(Lower *L, FeNode *n); +const char *drop_name(Lower *L, const FeType *t); +void run_deferred(Lower *L, unsigned from); +Slot wrap_context(Lower *L, Slot v, FeNode *n); +unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n); +Slot wrapper_payload(Lower *L, Slot w, const FeType *t); +void return_error(Lower *L, unsigned err, FeNode *n); +Slot lower_try(Lower *L, FeNode *n); +Slot lower_lazy(Lower *L, FeNode *n, int is_catch); +void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, + unsigned long size); +void lower_return(Lower *L, FeNode *n); +void lower_if(Lower *L, FeNode *n); +void lower_while(Lower *L, FeNode *n); +Slot lower_slice(Lower *L, FeNode *n); +void lower_for(Lower *L, FeNode *n); +void lower_match(Lower *L, FeNode *n); +void lower_stmt(Lower *L, FeNode *n); +void lower_global(Lower *L, FeNode *n); +int fn_is_generic(const FeNode *fn); +void lower_fn_as(Lower *L, FeNode *fn, const char *name); +void lower_fn(Lower *L, FeNode *fn); + +#endif diff --git a/fec/src/lowerprn.c b/fec/src/lowerprn.c new file mode 100644 index 0000000..eea3923 --- /dev/null +++ b/fec/src/lowerprn.c @@ -0,0 +1,236 @@ +#include "lowerpri.h" + +void emit_text(Lower *L, unsigned handle, const char *text, + unsigned long len) +{ + unsigned args[3]; + const char *label; + if (!len) return; + label = fe_ir_string(L->m, text, len); + if (!label) return; + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + args[1] = fe_ir_addr(L->m, L->b, fe_ir_at_global(label, 0)); + args[2] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)len); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write", args, 3); +} + +/* Write one value, the way the verb asked for. */ +void emit_value_text(Lower *L, unsigned handle, FeNode *arg, int verb) +{ + FeType *t = arg ? arg->sem_type : 0; + Slot v = lower_expr(L, arg); + unsigned args[3]; + if (t && (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR)) { + FeIrPlace at = v.place; + if (!v.is_place) { fail(L, "text with no place", arg); return; } + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + at.offset = v.place.offset + SLICE_PTR_OFFSET; + args[1] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); + at.offset = v.place.offset + SLICE_LEN_OFFSET; + args[2] = fe_ir_load(L->m, L->b, FE_IR_I32, at); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write", args, 3); + return; + } + if (t && t->kind == FE_TYPE_BOOL) { + /* Two literals and a branch: cheaper than a runtime that knows about + Ferro's names for truth. */ + FeIrBlock *yes = new_block(L); + FeIrBlock *no = new_block(L); + FeIrBlock *join = new_block(L); + fe_ir_br(L->b, as_value(L, v, arg), yes->id, no->id); + L->b = yes; + emit_text(L, handle, "true", 4); + fe_ir_jmp(L->b, join->id); + L->b = no; + emit_text(L, handle, "false", 5); + fe_ir_jmp(L->b, join->id); + L->b = join; + return; + } + if (verb == 'c' || (t && t->kind == FE_TYPE_CHAR)) { + /* One byte, written from a slot of its own so it has an address. */ + unsigned cell = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "char"); + fe_ir_store(L->m, L->b, fe_ir_at_local(cell, 0), as_value(L, v, arg), + FE_IR_I8); + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + args[1] = fe_ir_addr(L->m, L->b, fe_ir_at_local(cell, 0)); + args[2] = fe_ir_const(L->m, L->b, FE_IR_I32, 1); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write", args, 3); + return; + } + args[0] = fe_ir_const(L->m, L->b, FE_IR_I32, (long)handle); + args[1] = as_value(L, v, arg); + if (verb == 'x') { + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write_hex", args, 2); + return; + } + args[2] = fe_ir_const(L->m, L->b, FE_IR_I32, + type_is_unsigned(t) || (t && t->kind == FE_TYPE_ENUM) + ? 1 : 0); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_write_int", args, 3); +} + +/* `@print(fmt, ...)`, `@fprint(w, fmt, ...)`. The checker has already agreed + that the string is a literal and that the count matches. */ +int lower_print(Lower *L, FeNode *n, Slot *out) +{ + const char *name = n->text; + int to_writer; + unsigned handle; + FeNode *fmt; + FeNode *arg; + const char *text; + unsigned long raw; + unsigned long i; + unsigned long chunk; + char plain[1024]; + unsigned long plain_len; + if (!name || (strcmp(name, "@print") != 0 && strcmp(name, "@fprint") != 0)) + return 0; + to_writer = strcmp(name, "@fprint") == 0; + fmt = n->children; + if (to_writer) { + /* A Writer is a handle; Stdout is 1 and Stderr is 2 (std.io). */ + Slot w = lower_expr(L, fmt); + handle = 0; + (void)w; + fmt = fmt ? fmt->next : 0; + } + handle = to_writer ? 2 : 1; + if (!fmt || !fmt->text || fmt->text[0] != '"') { + fail(L, "a format string that is not a literal", n); + *out = slot_void(); + return 1; + } + text = fmt->text + 1; + raw = strlen(fmt->text); + if (raw >= 2) raw -= 2; + arg = fmt->next; + plain_len = 0; + chunk = 0; + (void)chunk; + for (i = 0; i < raw; ++i) { + char ch = text[i]; + if (ch == 92 && i + 1 < raw) { /* an escape */ + ++i; + switch (text[i]) { + case 'n': ch = 10; break; + case 't': ch = 9; break; + case 'r': ch = 13; break; + case '0': ch = 0; break; + default: ch = text[i]; break; + } + if (plain_len + 1 < sizeof plain) plain[plain_len++] = ch; + continue; + } + if (ch == '{') { + int verb = ' '; + unsigned long close = i + 1; + while (close < raw && text[close] != '}') ++close; + if (close == i + 2) verb = text[i + 1]; + emit_text(L, handle, plain, plain_len); + plain_len = 0; + emit_value_text(L, handle, arg, verb); + if (arg) arg = arg->next; + i = close; + continue; + } + if (ch == '}') continue; /* `}}` is one brace */ + if (plain_len + 1 < sizeof plain) plain[plain_len++] = ch; + } + emit_text(L, handle, plain, plain_len); + *out = slot_void(); + return 1; +} + +Slot lower_call(Lower *L, FeNode *n) +{ + unsigned args[16]; + unsigned count = 0; + FeNode *arg = n->children; + FeType *ret = n->sem_type; + FeIrType rt = ir_type(ret); + unsigned result_local = 0; + const char *callee = n->a && n->a->cname ? n->a->cname : + (n->sem_decl && n->sem_decl->cname ? + n->sem_decl->cname : 0); + { + Slot built; + if (lower_builtin(L, n, &built)) return built; + if (lower_mem(L, n, &built)) return built; + if (lower_print(L, n, &built)) return built; + } + if (!callee) { fail(L, "a call with no target", n); return slot_void(); } + /* An aggregate result is written through a hidden first argument. */ + if (rt == FE_IR_MEM) { + result_local = fe_ir_local(L->m, L->fn, FE_IR_MEM, ir_size(ret), + ir_align(ret), "result"); + args[count++] = fe_ir_addr(L->m, L->b, fe_ir_at_local(result_local, 0)); + } + /* A method call passes what it was reached through as its first argument. + `self: Self` and `self: &Self` are the same thing here: the address of + the receiver, because an aggregate never travels in a register. */ + if (n->a && n->a->kind == FE_N_MEMBER && n->sem_decl) { + FeNode *first = n->sem_decl->a ? n->sem_decl->a->children : 0; + if (first && first->text && !strcmp(first->text, "self")) { + FeType *rt = n->a->a ? n->a->a->sem_type : 0; + Slot recv = lower_expr(L, n->a->a); + /* A receiver that is already a reference or an owner is a pointer + already; taking its address would pass a pointer to the + pointer. */ + if (rt && (rt->kind == FE_TYPE_REF || + (rt->kind == FE_TYPE_OWNED && ir_type(rt) == FE_IR_PTR))) + args[count++] = as_value(L, recv, n->a->a); + else + args[count++] = recv.is_place ? as_address(L, recv, n->a->a) + : recv.temp; + } + } + /* A generic call passes its type arguments first. They were consumed when + the instance was chosen and carry no value, so they are not passed. */ + { + FeNode *p; + for (p = n->sem_decl && n->sem_decl->a ? n->sem_decl->a->children : 0; + p && arg; p = p->next) { + if (!(p->flags & FE_NODE_COMPTIME)) break; + arg = arg->next; + } + } + for (; arg; arg = arg->next) { + Slot a = lower_expr(L, arg); + if (count >= 16) { fail(L, "too many arguments", n); break; } + args[count++] = a.type == FE_IR_MEM ? as_address(L, a, arg) + : as_value(L, a, arg); + } + if (rt == FE_IR_MEM) { + fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); + return slot_place(fe_ir_at_local(result_local, 0), FE_IR_MEM, + ir_size(ret)); + } + if (rt == FE_IR_VOID) { + fe_ir_call(L->m, L->b, FE_IR_VOID, callee, args, count); + return slot_void(); + } + return slot_value(fe_ir_call(L->m, L->b, rt, callee, args, count), rt); +} + + +/* Every expression may be standing where a wrapper is expected, so the wrap is + applied once, here, rather than at each place that could need it. */ +Slot lower_expr(Lower *L, FeNode *n) +{ + Slot v; + if (!n || L->failed) return slot_void(); + v = lower_expr_core(L, n); + /* The checker marked the uses that hand ownership away. Where one names a + local we track, the value is no longer ours to release. */ + if ((n->flags & FE_OWN_NODE_CONSUMED) && n->kind == FE_N_IDENT) { + LowerVar *var = find_var(L, n->cname); + unsigned flag; + if (var && release_flag(L, var->local, &flag)) { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); + } + } + return n->sem_context ? wrap_context(L, v, n) : v; +} diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c new file mode 100644 index 0000000..1ac6395 --- /dev/null +++ b/fec/src/lowerstm.c @@ -0,0 +1,543 @@ +#include "lowerpri.h" + +void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, + unsigned long size) +{ + if (value.type == FE_IR_MEM) { + if (!value.is_place) { fail(L, "an aggregate value", n); return; } + fe_ir_copy(L->m, L->b, dst, value.place, size); + return; + } + fe_ir_store(L->m, L->b, dst, as_value(L, value, n), value.type); +} + +void lower_return(Lower *L, FeNode *n) +{ + Slot v; + if (!n->a) { + /* A bare return from a `!void` function still has to say that nothing + went wrong. */ + if (L->ret_type && L->ret_type->kind == FE_TYPE_ERROR_UNION) { + unsigned local = scratch(L, L->ret_type, "success"); + unsigned none = fe_ir_const(L->m, L->b, FE_IR_I16, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), none, FE_IR_I16); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + fe_ir_copy(L->m, L->b, fe_ir_at_temp(dst, 0), + fe_ir_at_local(local, 0), ir_size(L->ret_type)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, fe_ir_load(L->m, L->b, ir_type(L->ret_type), + fe_ir_at_local(local, 0)), 1); + return; + } + run_deferred(L, 0); + fe_ir_ret(L->b, 0, 0); + return; + } + /* The value is computed before the deferred blocks run, because they may + destroy what it was read from. */ + v = lower_expr(L, n->a); + if (v.type != FE_IR_MEM && v.is_place) + v = slot_value(as_value(L, v, n->a), v.type); + run_deferred(L, 0); + if (L->fn->returns_by_address) { + unsigned dst = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(L->ret_local, 0)); + store_into(L, fe_ir_at_temp(dst, 0), v, n, ir_size(L->ret_type)); + fe_ir_ret(L->b, 0, 0); + return; + } + fe_ir_ret(L->b, as_value(L, v, n->a), 1); +} + +void lower_if(Lower *L, FeNode *n) +{ + FeIrBlock *then_b = new_block(L); + FeIrBlock *else_b = n->c ? new_block(L) : 0; + FeIrBlock *join = new_block(L); + unsigned cond = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_br(L->b, cond, then_b->id, else_b ? else_b->id : join->id); + L->b = then_b; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, join->id); + if (else_b) { + L->b = else_b; + lower_stmt(L, n->c); + fe_ir_jmp(L->b, join->id); + } + L->b = join; +} + +void lower_while(Lower *L, FeNode *n) +{ + FeIrBlock *head = new_block(L); + FeIrBlock *body = new_block(L); + FeIrBlock *done = new_block(L); + unsigned cond; + fe_ir_jmp(L->b, head->id); + L->b = head; + cond = as_value(L, lower_expr(L, n->a), n->a); + fe_ir_br(L->b, cond, body->id, done->id); + if (L->loop_depth < 32) { + L->break_target[L->loop_depth] = done->id; + L->continue_target[L->loop_depth] = head->id; + ++L->loop_depth; + } + L->b = body; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, head->id); + if (L->loop_depth) --L->loop_depth; + L->b = done; +} + +/* `x[a..b]` makes a pointer and a length out of part of something indexable. + Both ends are checked -- against each other and against what is there -- + before the pointer is formed. An empty slice of a valid range is fine; one + that starts past its end is not. */ +Slot lower_slice(Lower *L, FeNode *n) +{ + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + FeType *t = n->sem_type; + Slot base = lower_expr(L, n->a); + unsigned data; + unsigned length; + unsigned from; + unsigned to; + unsigned local; + unsigned scale; + unsigned off; + unsigned at; + unsigned count; + indexable_parts(L, base, bt, &data, &length, n); + from = n->b ? as_value(L, lower_expr(L, n->b), n->b) + : fe_ir_const(L->m, L->b, FE_IR_I32, 0); + to = n->c ? as_value(L, lower_expr(L, n->c), n->c) : length; + if (!L->c->no_checks) { + unsigned ordered = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, + from, to, 1); + unsigned within; + guard(L, ordered, FE_TRAP_BOUNDS, n->loc.line); + within = fe_ir_binary(L->m, L->b, FE_IR_LE, FE_IR_I32, to, length, 1); + guard(L, within, FE_TRAP_BOUNDS, n->loc.line); + } + scale = fe_ir_const(L->m, L->b, FE_IR_I32, (long)ir_size(elem)); + off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, from, scale, 1); + at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, data, off, 1); + count = fe_ir_binary(L->m, L->b, FE_IR_SUB, FE_IR_I32, to, from, 1); + local = scratch(L, t, "slice"); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_PTR_OFFSET), at, + FE_IR_PTR); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, SLICE_LEN_OFFSET), count, + FE_IR_I32); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); +} + +/* Three shapes share the keyword. + + for i in a..b { } counts + for x in thing { } walks, binding a reference to each element + for i, x in thing { } walks, binding the position as well + + The count is read once before the body, so a thing that grows underneath the + loop cannot walk past what was measured. The element binding is a reference + (`x.^` reads it), which is what lets a loop write back into the thing. */ +void lower_for(Lower *L, FeNode *n) +{ + FeIrBlock *head; + FeIrBlock *body; + FeIrBlock *step; + FeIrBlock *done; + unsigned counter; + unsigned limit; + + if (n->c) { + /* The counting form: the variable is the count itself. */ + unsigned from = as_value(L, lower_expr(L, n->a), n->a); + unsigned to; + counter = declare_var(L, n->cname, 0, n->text); + L->fn->locals[counter].type = FE_IR_I32; + L->fn->locals[counter].size = 4; + L->fn->locals[counter].align = 4; + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), from, FE_IR_I32); + to = as_value(L, lower_expr(L, n->c), n->c); + limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "limit"); + fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), to, FE_IR_I32); + head = new_block(L); + body = new_block(L); + step = new_block(L); + done = new_block(L); + fe_ir_jmp(L->b, head->id); + L->b = head; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(limit, 0)); + unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); + fe_ir_br(L->b, more, body->id, done->id); + } + } else { + FeType *bt = n->a ? n->a->sem_type : 0; + FeType *elem = bt ? bt->elem : 0; + Slot base = lower_expr(L, n->a); + unsigned data; + unsigned length; + unsigned data_local; + unsigned item; + indexable_parts(L, base, bt, &data, &length, n); + data_local = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, "data"); + fe_ir_store(L->m, L->b, fe_ir_at_local(data_local, 0), data, FE_IR_PTR); + limit = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "count"); + fe_ir_store(L->m, L->b, fe_ir_at_local(limit, 0), length, FE_IR_I32); + /* With two names the first is the position and the second the element; + with one it is the element. */ + counter = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "index"); + if (n->aux_cname) { + L->vars[L->var_count].cname = n->cname; + L->vars[L->var_count].local = counter; + L->vars[L->var_count].by_address = 0; + if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->aux_text); + L->vars[L->var_count].cname = n->aux_cname; + L->vars[L->var_count].local = item; + L->vars[L->var_count].by_address = 0; + if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + } else { + item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->text); + L->vars[L->var_count].cname = n->cname; + L->vars[L->var_count].local = item; + L->vars[L->var_count].by_address = 0; + if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + } + { + unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I32, 0); + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), zero, FE_IR_I32); + } + head = new_block(L); + body = new_block(L); + step = new_block(L); + done = new_block(L); + fe_ir_jmp(L->b, head->id); + L->b = head; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned e = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(limit, 0)); + unsigned more = fe_ir_binary(L->m, L->b, FE_IR_LT, FE_IR_I32, i, e, 1); + fe_ir_br(L->b, more, body->id, done->id); + } + L->b = body; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned scale = fe_ir_const(L->m, L->b, FE_IR_I32, + (long)ir_size(elem)); + unsigned off = fe_ir_binary(L->m, L->b, FE_IR_MUL, FE_IR_I32, i, + scale, 1); + unsigned p = fe_ir_load(L->m, L->b, FE_IR_PTR, + fe_ir_at_local(data_local, 0)); + unsigned at = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_PTR, p, + off, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(item, 0), at, FE_IR_PTR); + } + L->b = head; + } + + if (L->loop_depth < 32) { + L->break_target[L->loop_depth] = done->id; + L->continue_target[L->loop_depth] = step->id; + ++L->loop_depth; + } + L->b = body; + lower_stmt(L, n->b); + fe_ir_jmp(L->b, step->id); + L->b = step; + { + unsigned i = fe_ir_load(L->m, L->b, FE_IR_I32, + fe_ir_at_local(counter, 0)); + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I32, 1); + unsigned next = fe_ir_binary(L->m, L->b, FE_IR_ADD, FE_IR_I32, i, one, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(counter, 0), next, FE_IR_I32); + } + fe_ir_jmp(L->b, head->id); + if (L->loop_depth) --L->loop_depth; + L->b = done; +} + +/* `match` over a payload-free enum or an integer: compare the tag against each + arm's pattern in turn. The checker already proved the arms cover everything, + so falling off the end cannot happen in a program that compiled -- but the + generated code has to go somewhere, and going to the join is right. */ +void lower_match(Lower *L, FeNode *n) +{ + FeType *t = n->a ? n->a->sem_type : 0; + FeIrType it = ir_type(t); + Slot subject = lower_expr(L, n->a); + unsigned value; + FeIrBlock *join; + FeNode *arm; + if (it == FE_IR_MEM) { fail(L, "a match over a payload", n); return; } + value = as_value(L, subject, n->a); + join = new_block(L); + for (arm = n->children; arm; arm = arm->next) { + FeIrBlock *body; + FeIrBlock *next; + unsigned want; + unsigned same; + FeVariantType *v; + if (arm->kind != FE_N_ARM) continue; + if (arm->text && !strcmp(arm->text, "_")) { + lower_stmt(L, arm->a); + fe_ir_jmp(L->b, join->id); + L->b = join; + return; + } + v = t && t->kind == FE_TYPE_ENUM && arm->text + ? fe_type_variant(t, arm->text) : 0; + want = fe_ir_const(L->m, L->b, it, + v ? (long)v->tag : literal_value(arm)); + same = fe_ir_binary(L->m, L->b, FE_IR_EQ, it, value, want, 1); + body = new_block(L); + next = new_block(L); + fe_ir_br(L->b, same, body->id, next->id); + L->b = body; + lower_stmt(L, arm->a); + fe_ir_jmp(L->b, join->id); + L->b = next; + } + fe_ir_jmp(L->b, join->id); + L->b = join; +} + +void lower_stmt(Lower *L, FeNode *n) +{ + FeNode *x; + if (!n || L->failed) return; + switch (n->kind) { + case FE_N_BLOCK: { + unsigned outer = L->owed_count; + for (x = n->children; x; x = x->next) lower_stmt(L, x); + /* Leaving a block normally settles what it owes. An exit that jumped + away already settled on its way out. */ + if (!L->b->terminated) run_deferred(L, outer); + L->owed_count = outer; + return; + } + case FE_N_LET: + case FE_N_VAR: + case FE_N_CONST: { + unsigned local = declare_var(L, n->cname, n->sem_type, n->text); + if (n->b) { + Slot v = lower_expr(L, n->b); + unsigned flag; + store_into(L, fe_ir_at_local(local, 0), v, n, ir_size(n->sem_type)); + if (release_flag(L, local, &flag)) { + unsigned one = fe_ir_const(L->m, L->b, FE_IR_I8, 1); + fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), one, FE_IR_I8); + } + } + return; + } + case FE_N_ASSIGN: { + Slot dst = lower_expr(L, n->a); + Slot v = lower_expr(L, n->b); + if (!dst.is_place) { fail(L, "an assignment to a value", n); return; } + store_into(L, dst.place, v, n, dst.size); + return; + } + case FE_N_EXPR_STMT: + lower_expr(L, n->a); + return; + case FE_N_RETURN: + lower_return(L, n); + return; + case FE_N_IF: + lower_if(L, n); + return; + case FE_N_WHILE: + lower_while(L, n); + return; + case FE_N_BREAK: + if (L->loop_depth) fe_ir_jmp(L->b, L->break_target[L->loop_depth - 1]); + return; + case FE_N_CONTINUE: + if (L->loop_depth) + fe_ir_jmp(L->b, L->continue_target[L->loop_depth - 1]); + return; + case FE_N_UNSAFE: + lower_stmt(L, n->a); + return; + case FE_N_DEFER: + if (L->owed_count < 64) { + L->owed[L->owed_count].block = n->a; + L->owed[L->owed_count].local = 0; + L->owed[L->owed_count].flag = 0; + L->owed[L->owed_count].type = 0; + ++L->owed_count; + } + return; + case FE_N_FOR: + lower_for(L, n); + return; + case FE_N_MATCH: + lower_match(L, n); + return; + default: + fail(L, "this statement", n); + return; + } +} + +/* ------------------------------------------------------------ functions --- */ + +/* A global is static storage. SPEC 7.1: its initializer is evaluated at + compile time, so what reaches here is either a constant to place in the + image or nothing, and the storage starts as zeroes. */ +void lower_global(Lower *L, FeNode *n) +{ + FeType *t = n->sem_type; + unsigned char *init = 0; + unsigned long size = ir_size(t); + if (!n->cname) return; + if (n->b && n->b->kind == FE_N_LITERAL && size && size <= 8) { + long v = literal_value(n->b); + unsigned long i; + init = (unsigned char *)fe_arena_alloc(&L->m->arena, (size_t)size); + if (init) + for (i = 0; i < size; ++i) + init[i] = (unsigned char)((v >> (i * 8)) & 0xFF); + } + fe_ir_global(L->m, n->cname, ir_type(t), size, ir_align(t), init); +} + +int fn_is_generic(const FeNode *fn) +{ + FeNode *p; + if (!fn) return 0; + for (p = fn->a ? fn->a->children : 0; p; p = p->next) + if (p->flags & FE_NODE_COMPTIME) return 1; + return 0; +} + +void lower_fn_as(Lower *L, FeNode *fn, const char *name) +{ + FeNode *p; + FeType *ret = fn->b ? fe_type_from_ast(&L->c->types, fn->b) : 0; + FeIrFunc *f; + if (!name) return; + f = fe_ir_func(L->m, name, ir_type(ret), ir_size(ret)); + if (!f) return; + L->fn = f; + L->ret_type = ret; + L->var_count = 0; + L->loop_depth = 0; + /* A hidden first parameter holds where an aggregate result goes. */ + if (f->returns_by_address) + L->ret_local = fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, "result"); + for (p = fn->a ? fn->a->children : 0; p; p = p->next) { + FeType *pt; + int by_address; + unsigned local; + /* A comptime parameter was consumed at compile time; it has no + storage and takes no argument slot. */ + if (p->flags & FE_NODE_COMPTIME) continue; + pt = fe_type_from_ast(&L->c->types, p->a); + /* An aggregate parameter arrives as an address. */ + by_address = ir_type(pt) == FE_IR_MEM; + local = by_address + ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) + : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), + p->text); + if (L->var_count < LOWER_MAX_LOCALS) { + L->vars[L->var_count].cname = p->cname; + L->vars[L->var_count].local = local; + L->vars[L->var_count].by_address = by_address; + ++L->var_count; + } + } + f->param_count = f->local_count; + L->b = fe_ir_block(L->m, f); + lower_stmt(L, fn->c); + /* A void function may just run off the end. */ + fe_ir_ret(L->b, 0, 0); +} + +void lower_fn(Lower *L, FeNode *fn) +{ + lower_fn_as(L, fn, fn->cname); +} + +int fe_lower_program(FeCheck *c, FeIrModule *out) +{ + Lower L; + unsigned u; + FeNode *n; + memset(&L, 0, sizeof L); + L.c = c; + L.m = out; + /* The codes have to be known while the bodies are lowered, so the names + are gathered from the whole build first. */ + for (u = 0; u < c->build->count; ++u) + collect_error_names(&L, c->build->units[u].ast.root); + for (u = 0; u < c->build->count; ++u) { + FeUnit *unit = &c->build->units[u]; + c->ast = &unit->ast; + c->unit = unit; + c->types.unit_name = unit->name[0] ? unit->name : "unit"; + if (!out->unit_file || !out->unit_file[0]) out->unit_file = unit->path; + for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) + if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) + lower_global(&L, n); + else if (n->kind == FE_N_FN && !n->c) { + /* A declaration with no body is something the linker will + find: the runtime, or a C library. */ + FeType *ret = n->b ? fe_type_from_ast(&c->types, n->b) : 0; + FeIrFunc *f; + if (!n->cname) continue; + f = fe_ir_func(out, n->cname, ir_type(ret), ir_size(ret)); + if (f) f->is_extern = 1; + } + else if (n->kind == FE_N_FN && n->c && !fn_is_generic(n)) { + lower_fn(&L, n); + /* The entry unit is the one the build was rooted at. */ + if (u == 0 && n->text && !strcmp(n->text, "main")) + out->entry_main = n->cname; + } + } + /* Each instance the checker reached is a function of its own: the same + body, read with different types bound, under its own link name. This is + where monomorphisation actually produces code -- the front end only + decided which instances exist. */ + for (u = 0; u < c->instance_count && !L.failed; ++u) { + FeInstance *inst = &c->instances[u]; + FeUnit *home; + FeTypeBind save[FE_TYPE_PARAM_MAX]; + unsigned save_count; + unsigned k; + if (!inst->decl || !inst->decl->c || !inst->cname || !inst->home) + continue; + home = 0; + for (k = 0; k < c->build->count; ++k) + if (!strcmp(c->build->units[k].name, inst->home)) + home = &c->build->units[k]; + if (!home) continue; + c->ast = &home->ast; + c->unit = home; + c->types.unit_name = home->name; + save_count = c->types.param_count; + for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) save[k] = c->types.params[k]; + c->types.param_count = inst->bind_count; + for (k = 0; k < inst->bind_count && k < FE_TYPE_PARAM_MAX; ++k) + c->types.params[k] = inst->binds[k]; + lower_fn_as(&L, inst->decl, inst->cname); + c->types.param_count = save_count; + for (k = 0; k < FE_TYPE_PARAM_MAX; ++k) c->types.params[k] = save[k]; + } + return !L.failed; +} diff --git a/tests/run.py b/tests/run.py index 4e50fdd..913c2fd 100644 --- a/tests/run.py +++ b/tests/run.py @@ -33,7 +33,8 @@ ROOT = Path(__file__).resolve().parent.parent FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", - "check", "resolve", "ir", "lower", "x86", "driver") + "check", "checkexp", "checkstm", "checkgen", "checkcal", "checkpro", + "resolve", "ir", "lower", "lowerprn", "lowerexp", "lowerstm", "x86", "driver") MARKER = re.compile(r"^//\s*ERROR:(?:(\d+):)?(.*)$") From e014c95a750254454e0502c545d73a2045644e83 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:05:55 +0900 Subject: [PATCH 153/184] =?UTF-8?q?lower:=20@print=20=EA=B3=BC=20@fprint?= =?UTF-8?q?=20=EB=A5=BC=20=EC=BB=B4=ED=8C=8C=EC=9D=BC=20=EB=8B=A8=EA=B3=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=A0=84=EA=B0=9C=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC 6.3.1: 포매팅 빌트인은 가변 인자 함수가 아니다. 호출 하나가 리터럴 조각마다 쓰기 하나, 값마다 쓰기 하나로 펴진다. 언어에 가변 인자 호출 규약이 생기지 않고, 포맷 문자열은 실행 시점에 이미 사라져 있다. verb: {} 십진, {x} 16진, {c} 한 바이트, 문자열은 포인터와 길이, bool 은 분기 두 개와 리터럴 두 개. 코드 생성기가 호출됐지만 정의되지 않은 이름을 스스로 extern 선언한다. lowering 이 런타임 호출을 직접 내므로, 손으로 관리해야 하는 목록 대신 호출 자체에서 이름을 모은다. @print("n={} neg={} hex={x}\n", 42, 0-7, 255) -> n=42 neg=-7 hex=ff --- fec/src/x86.c | 24 ++++++++++++++++++++++++ fec/tests/exec/printfmt.fe | 14 ++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 fec/tests/exec/printfmt.fe diff --git a/fec/src/x86.c b/fec/src/x86.c index ca115de..065e507 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -360,6 +360,30 @@ void fe_x86_emit(const FeIrModule *m, FILE *out) for (f = m->funcs; f; f = f->next) if (f->is_extern || !f->first) fprintf(out, "extern %s : near\n", f->name); + /* Anything called but not defined here lives somewhere else -- the runtime, + or a library. Lowering emits such calls directly (allocating, writing, + trapping), so the names are collected from the calls themselves rather + than from a list that would have to be kept in step. */ + { + const char *seen[64]; + unsigned count = 0; + const FeIrValue *v; + const FeIrFunc *g; + unsigned i; + for (f = m->funcs; f; f = f->next) + for (b = f->first; b; b = b->next) + for (v = b->first; v; v = v->next) { + if (v->op != FE_IR_CALL || !v->callee) continue; + for (g = m->funcs; g; g = g->next) + if (!strcmp(g->name, v->callee)) break; + if (g) continue; + for (i = 0; i < count; ++i) + if (!strcmp(seen[i], v->callee)) break; + if (i < count || count >= 64) continue; + seen[count++] = v->callee; + fprintf(out, "extern %s : near\n", v->callee); + } + } if (any_trap) fputs("extern fe_trap : near\n", out); fputs("\n_DATA segment dword public 'DATA'\n", out); diff --git a/fec/tests/exec/printfmt.fe b/fec/tests/exec/printfmt.fe new file mode 100644 index 0000000..31d69b3 --- /dev/null +++ b/fec/tests/exec/printfmt.fe @@ -0,0 +1,14 @@ +// EXIT:0 +// OUTPUT:n=42 neg=-7 hex=ff +// OUTPUT:yes=true no=false +// OUTPUT:text=hello char=A +// OUTPUT:braces {} done +unit printfmt; + +fn main() -> i32 { + @print("n={} neg={} hex={x}\n", 42, 0 - 7, 255); + @print("yes={} no={}\n", true, false); + @print("text={} char={c}\n", "hello", 'A'); + @print("braces {} done\n", "{}"); + return 0; +} From 676fef88fb2039b7e62c1f7976e0802a90384720 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:11:56 +0900 Subject: [PATCH 154/184] =?UTF-8?q?std:=20=ED=94=84=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EB=9E=A8=EC=9D=B4=20=EB=B0=94=EA=B9=A5=20=EC=84=B8=EC=83=81?= =?UTF-8?q?=EA=B3=BC=20=EC=9D=B4=EC=95=BC=EA=B8=B0=ED=95=9C=EB=8B=A4=20--?= =?UTF-8?q?=20=ED=8C=8C=EC=9D=BC=EA=B3=BC=20=EB=AA=85=EB=A0=B9=EC=A4=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 런타임에 open/read/close 와 명령줄을 넣었다. std.io 가 그 위에 파일 열기, 읽기, 쓰기, 그리고 명령줄을 조각으로 나누는 것을 얹는다. 인용부호 처리는 런타임이 알 일이 아니라 라이브러리가 할 일이다. 길에서 고친 것들: - *T 가 타입 시스템에 실체가 없어서 덩어리로 취급됐다. 이제 진짜 종류다 -- 주소일 뿐이고 추적할 대여도 실행할 drop 도 없는 Copy 타입. 그 결과 &u8 이 *u8 에 자동으로 맞지 않게 됐는데, 그게 맞다: R9 는 그 변환을 unsafe 안의 @ptr_cast 로만 허용한다. - raw 포인터에 정수를 더하면 더 뒤의 주소다. 소유자나 대여에는 허용하지 않는다 -- 자기 자리가 있는 것에서 걸어나가는 것이 *T 의 용도다. - @volatile_load / @volatile_store / @ptr_cast 를 내린다. - undefined 가 선언된 타입을 따른다. 없으면 손으로 타이핑할 수 있는 것보다 큰 버퍼를 선언할 방법이 아예 없었다. R8 이 정확히 동작하는 것도 확인했다: 참조성 파라미터가 둘인 함수는 슬라이스를 반환할 수 없다. 어디서 파생됐는지 시그니처가 말하지 않기 때문이다. exec.py 24/24. --- fec/rt/start.asm | 76 +++++++++++++++++++++++++++++ fec/src/checkcal.c | 8 ++++ fec/src/checkexp.c | 13 +++++ fec/src/checkstm.c | 8 ++++ fec/src/lower.c | 33 ++++++++++++- fec/src/lowerstm.c | 4 ++ fec/src/types.c | 19 +++++++- fec/src/types.h | 7 ++- fec/std/io.fe | 97 +++++++++++++++++++++++++++++++++++++- fec/std/sys.fe | 22 +++++++++ fec/tests/exec/cmdargs.fe | 21 +++++++++ fec/tests/exec/readfile.fe | 37 +++++++++++++++ 12 files changed, 340 insertions(+), 5 deletions(-) create mode 100644 fec/tests/exec/cmdargs.fe create mode 100644 fec/tests/exec/readfile.fe diff --git a/fec/rt/start.asm b/fec/rt/start.asm index c2726e6..27fe54c 100644 --- a/fec/rt/start.asm +++ b/fec/rt/start.asm @@ -131,6 +131,10 @@ fe_trap endp extern _GetProcessHeap@0 : near extern _HeapAlloc@12 : near extern _HeapFree@12 : near +extern _CreateFileA@28 : near +extern _ReadFile@20 : near +extern _CloseHandle@4 : near +extern _GetCommandLineA@0 : near ; fe_rt_write(handle, ptr, len) -> bytes written public fe_rt_write @@ -298,6 +302,78 @@ hex_digit: ret fe_rt_write_hex endp +; fe_rt_open(path, write) -> handle, or -1 +; `path` is a NUL-terminated byte string. Reading opens what is there; writing +; creates or truncates. +public fe_rt_open +fe_rt_open proc near + push ebp + mov ebp, esp + push 0 ; hTemplateFile + push 128 ; FILE_ATTRIBUTE_NORMAL + cmp dword ptr [ebp+12], 0 + jne open_write + push 3 ; OPEN_EXISTING + push 0 + push 1 ; FILE_SHARE_READ + push 80000000h ; GENERIC_READ + jmp open_call +open_write: + push 2 ; CREATE_ALWAYS + push 0 + push 0 + push 40000000h ; GENERIC_WRITE +open_call: + push dword ptr [ebp+8] + call _CreateFileA@28 + mov esp, ebp + pop ebp + ret +fe_rt_open endp + +; fe_rt_read(handle, buf, len) -> bytes read, or -1 +public fe_rt_read +fe_rt_read proc near + push ebp + mov ebp, esp + push 0 + push offset written + push dword ptr [ebp+16] + push dword ptr [ebp+12] + push dword ptr [ebp+8] + call _ReadFile@20 + test eax, eax + jne read_ok + mov eax, -1 + jmp read_done +read_ok: + mov eax, [written] +read_done: + mov esp, ebp + pop ebp + ret +fe_rt_read endp + +; fe_rt_close(handle) +public fe_rt_close +fe_rt_close proc near + push ebp + mov ebp, esp + push dword ptr [ebp+8] + call _CloseHandle@4 + mov esp, ebp + pop ebp + ret +fe_rt_close endp + +; fe_rt_cmdline() -> pointer to the whole command line, NUL terminated. +; Splitting it is the standard library's job, not the runtime's. +public fe_rt_cmdline +fe_rt_cmdline proc near + call _GetCommandLineA@0 + ret +fe_rt_cmdline endp + ; fe_rt_exit(code) -- never returns public fe_rt_exit fe_rt_exit proc near diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index 11e1d50..3dfb14b 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -444,6 +444,14 @@ FeType *check_expr(FeCheckerState *s, FeNode *n) n->sem_type=fe_type_intern(&s->c->types,"bool"); return n->sem_type; } + /* A raw pointer plus a number is an address further along. Only raw + pointers: an owner or a borrow has a place it belongs to, and + walking away from it is what `*T` is for. */ + if (known(a) && a->kind==FE_TYPE_RAW && known(b) && + fe_type_is_integer(b) && op[0] && (op[0]=='+' || op[0]=='-')) { + n->sem_type=a; + return n->sem_type; + } if ((known(a) && !fe_type_is_integer(a)) || (known(b) && !fe_type_is_integer(b)) || (known(a) && known(b) && !fe_type_equal(a,b) && diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index 33f18c9..a8a9af0 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -495,6 +495,19 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) if(!target || !known(target)) err(c,n->loc,"size/align requires a known type"); n->sem_type=fe_type_intern(&c->types,"usize"); return n->sem_type; } + if (!n->a && n->text && strcmp(n->text,"@ptr_cast")==0) { + /* `@ptr_cast(T, p)`: the first argument names the type the result + points at, the second is the address. R9 keeps it in `unsafe`. */ + FeNode *type_arg=n->children; + FeNode *value=type_arg ? type_arg->next : 0; + FeType *target=type_arg && type_arg->kind==FE_N_IDENT ? + fe_type_intern(&c->types,type_arg->text) : unknown(c); + if (!type_arg || !value || value->next) + err(c,n->loc,"@ptr_cast requires a type and a pointer"); + if (value) check_expr(s,value); + n->sem_type=fe_type_raw(&c->types,target); + return n->sem_type; + } if (n->a && n->a->kind == FE_N_MEMBER) { FeNode *method; FeNode *self_param; diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 587e199..b3b1e0a 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -535,6 +535,14 @@ FeType *m7_check_expected(FeCheckerState *s, FeNode *value, FeType *actual; FeM7ContextKind context; if (!value) return unknown(s->c); + /* `undefined` is not a value, it is the absence of one: it takes whatever + type was asked for, and says the storage starts out unset. Without this + there is no way to declare a buffer larger than you care to type out. */ + if (value->kind==FE_N_LITERAL && value->text && + !strcmp(value->text,"undefined") && expected) { + value->sem_type=expected; + return expected; + } if (fe_m7_is_null(value)) { if (!fe_m7_can_contextual_null(expected)) { err(s->c,value->loc,"null requires a contextual optional type"); diff --git a/fec/src/lower.c b/fec/src/lower.c index da94f4d..045197e 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -25,7 +25,8 @@ FeIrType ir_type_of(const FeType *t) if (t->bits <= 8U) return FE_IR_I8; if (t->bits <= 16U) return FE_IR_I16; return FE_IR_I32; - case FE_TYPE_REF: return FE_IR_PTR; + case FE_TYPE_REF: + case FE_TYPE_RAW: return FE_IR_PTR; case FE_TYPE_OWNED: /* An owned slice carries a length beside the pointer. */ return t->elem && t->elem->kind == FE_TYPE_SLICE ? FE_IR_MEM : FE_IR_PTR; @@ -391,6 +392,36 @@ int lower_builtin(Lower *L, FeNode *n, Slot *out) *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, v), FE_IR_I32); return 1; } + if (!strcmp(name, "@volatile_load")) { + /* Reading through a raw pointer. Nothing here reorders loads yet, so + volatile and ordinary read the same; the keyword is what marks the + access as deliberate, and the checker already required `unsafe`. */ + FeNode *arg = n->children; + unsigned p = as_value(L, lower_expr(L, arg), arg); + FeIrType t = ir_type(n->sem_type); + if (t == FE_IR_VOID || t == FE_IR_MEM) t = FE_IR_I8; + *out = slot_place(fe_ir_at_temp(p, 0), t, ir_size(n->sem_type)); + return 1; + } + if (!strcmp(name, "@volatile_store")) { + FeNode *arg = n->children; + FeNode *value = arg ? arg->next : 0; + unsigned p = as_value(L, lower_expr(L, arg), arg); + Slot v = lower_expr(L, value); + FeIrType t = value && value->sem_type ? ir_type(value->sem_type) + : FE_IR_I8; + fe_ir_store(L->m, L->b, fe_ir_at_temp(p, 0), as_value(L, v, value), t); + *out = slot_void(); + return 1; + } + if (!strcmp(name, "@ptr_cast")) { + /* A pointer is a pointer; the type it is said to point at is the + checker's business and leaves no trace here. */ + FeNode *arg = n->children; + FeNode *value = arg ? arg->next : 0; + *out = slot_value(as_value(L, lower_expr(L, value), value), FE_IR_PTR); + return 1; + } if (!strcmp(name, "@line")) { *out = slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, (long)n->loc.line), FE_IR_I32); diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index 1ac6395..ff93d4d 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -333,6 +333,10 @@ void lower_stmt(Lower *L, FeNode *n) case FE_N_VAR: case FE_N_CONST: { unsigned local = declare_var(L, n->cname, n->sem_type, n->text); + /* `undefined` says the storage starts out unset, so there is nothing + to write into it. */ + if (n->b && n->b->kind == FE_N_LITERAL && n->b->text && + !strcmp(n->b->text, "undefined")) return; if (n->b) { Slot v = lower_expr(L, n->b); unsigned flag; diff --git a/fec/src/types.c b/fec/src/types.c index 71ef161..4572476 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -234,6 +234,21 @@ FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable) return t; } +FeType *fe_type_raw(FeTypeCtx *ctx, FeType *elem) +{ + char key[320]; + FeType *t; + sprintf(key, "*%s", elem ? elem->name : "?"); + t = fe_type_intern(ctx, key); + if (t->kind == FE_TYPE_UNKNOWN) { + t->kind = FE_TYPE_RAW; + t->elem = elem; + t->size = FE_PTR_SIZE; + t->align = FE_PTR_ALIGN; + } + return t; +} + FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem) { char key[320]; @@ -485,7 +500,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) if (t->size > 4UL) t->size = 4UL; t->cycle_state = 2; return; } - if (t->kind == FE_TYPE_REF) { + if (t->kind == FE_TYPE_REF || t->kind == FE_TYPE_RAW) { t->size = FE_PTR_SIZE; t->align = FE_PTR_ALIGN; t->cycle_state = 2; return; @@ -613,7 +628,7 @@ FeType *fe_type_from_ast(FeTypeCtx *ctx, const FeNode *node) return fe_type_error_union(ctx,fe_type_from_ast(ctx,node->a)); } if (node->text && strcmp(node->text, "*") == 0) - return fe_type_intern(ctx, ""); + return fe_type_raw(ctx, fe_type_from_ast(ctx, node->a)); if (node->text && strcmp(node->text, "fn") == 0) return fe_type_intern(ctx, ""); /* A plain named type may be a generic declaration -- with arguments it is diff --git a/fec/src/types.h b/fec/src/types.h index 4a4a4a1..4109839 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -7,7 +7,11 @@ typedef enum FeTypeKind { FE_TYPE_ERROR, FE_TYPE_ERROR_UNION, FE_TYPE_OPTIONAL, FE_TYPE_VOID, FE_TYPE_BOOL, FE_TYPE_CHAR, FE_TYPE_INT, FE_TYPE_STRUCT, FE_TYPE_ENUM, FE_TYPE_ARRAY, FE_TYPE_SLICE, FE_TYPE_STR, - FE_TYPE_REF, FE_TYPE_OWNED, FE_TYPE_UNKNOWN + FE_TYPE_REF, FE_TYPE_OWNED, + /* `*T`. A machine address and nothing else: no borrow to track, no drop + to run, Copy. Everything it is good for is behind `unsafe`. */ + FE_TYPE_RAW, + FE_TYPE_UNKNOWN } FeTypeKind; /* One target, one pointer width (SPEC 2). usize and isize are that width and @@ -124,6 +128,7 @@ FeType *fe_type_slice(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_mut_slice(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_ref(FeTypeCtx *ctx, FeType *elem, int mutable); FeType *fe_type_owned(FeTypeCtx *ctx, FeType *elem); +FeType *fe_type_raw(FeTypeCtx *ctx, FeType *elem); FeType *fe_type_error_union(FeTypeCtx *ctx, FeType *value); void fe_type_require_replace(FeTypeCtx *ctx, FeType *type); FeType *fe_type_declare_struct(FeTypeCtx *ctx, const FeNode *node, int packed); diff --git a/fec/std/io.fe b/fec/std/io.fe index 475b1d8..75e2cde 100644 --- a/fec/std/io.fe +++ b/fec/std/io.fe @@ -10,7 +10,8 @@ pub fn write(w: Writer, bytes: []u8) -> usize { if w == Writer.Null { return bytes.n; } var handle: i32 = 1; if w == Writer.Stderr { handle = 2; } - let done: i32 = sys.raw_write(handle, &bytes[0], bytes.n); + var done: i32 = 0; + unsafe { done = sys.raw_write(handle, @ptr_cast(u8, &bytes[0]), bytes.n); } if done < 0 { return 0; } return done as usize; } @@ -23,3 +24,97 @@ pub fn println(bytes: []u8) -> usize { let n: usize = write(Writer.Stdout, bytes); return n + write(Writer.Stdout, "\n"); } + +// Files. A handle is what the operating system gave back; -1 means it did not +// give one. The path has to be NUL terminated because that is what the system +// call wants, and `to_cstr` is how a Ferro string becomes one. + +pub fn open_read(path: []mut u8) -> !i32 { + var handle: i32 = 0; + unsafe { handle = sys.raw_open(@ptr_cast(u8, &path[0]), 0); } + if handle == 0 - 1 { return error.NoSuchFile; } + return handle; +} + +pub fn open_write(path: []mut u8) -> !i32 { + var handle: i32 = 0; + unsafe { handle = sys.raw_open(@ptr_cast(u8, &path[0]), 1); } + if handle == 0 - 1 { return error.CannotWrite; } + return handle; +} + +pub fn read(handle: i32, into: []mut u8) -> !usize { + var got: i32 = 0; + unsafe { got = sys.raw_read(handle, @ptr_cast(u8, &into[0]), into.n); } + if got < 0 { return error.ReadFailed; } + return got as usize; +} + +pub fn close(handle: i32) -> void { + sys.raw_close(handle); +} + +/// Put `text` into `buf` with a NUL after it and say how many bytes that took, +/// the NUL included. A system call cannot be told a length, so it needs this. +/// +/// The length comes back rather than a slice of `buf`: with two reference-like +/// parameters the signature cannot say which one a returned slice came from, +/// and R8 will not guess. +pub fn to_cstr(buf: []mut u8, text: []u8) -> usize { + var i: usize = 0; + while i < text.n { + if i + 1 >= buf.n { break; } + buf[i] = text[i]; + i = i + 1; + } + if i < buf.n { buf[i] = 0; } + return i + 1; +} + +pub fn write_file(handle: i32, bytes: []u8) -> !usize { + var done: i32 = 0; + unsafe { done = sys.raw_write(handle, @ptr_cast(u8, &bytes[0]), bytes.n); } + if done < 0 { return error.WriteFailed; } + return done as usize; +} + +/// Copy the command line into `buf` and say how long it is. It arrives as one +/// string with the program's own name first; `arg` picks a piece out of it. +pub fn cmdline(buf: []mut u8) -> usize { + let raw: *u8 = sys.raw_cmdline(); + var i: usize = 0; + unsafe { + while i + 1 < buf.n { + let c: u8 = @volatile_load(raw + i); + if c == 0 { break; } + buf[i] = c; + i = i + 1; + } + } + return i; +} + +/// The `n`th whitespace-separated piece of `line`, or an empty slice when +/// there is no such piece. Quoting is not handled; nothing here needs it yet. +pub fn arg(line: []u8, n: usize) -> []u8 { + var at: usize = 0; + var seen: usize = 0; + while at < line.n { + while at < line.n { + if line[at] != 32 { break; } + at = at + 1; + } + var stop: usize = at; + while stop < line.n { + if line[stop] == 32 { break; } + stop = stop + 1; + } + if stop > at { + if seen == n { return line[at..stop]; } + seen = seen + 1; + } + at = stop; + } + return line[0..0]; +} + diff --git a/fec/std/sys.fe b/fec/std/sys.fe index c2c91f9..53c3db0 100644 --- a/fec/std/sys.fe +++ b/fec/std/sys.fe @@ -8,6 +8,10 @@ extern "c" fn fe_rt_free(p: *u8); extern "c" fn fe_rt_exit(code: i32); extern "c" fn fe_rt_allocs() -> i32; extern "c" fn fe_rt_frees() -> i32; +extern "c" fn fe_rt_open(path: *u8, write: i32) -> i32; +extern "c" fn fe_rt_read(handle: i32, buf: *u8, len: usize) -> i32; +extern "c" fn fe_rt_close(handle: i32); +extern "c" fn fe_rt_cmdline() -> *u8; pub fn exit(code: i32) -> void { unsafe { fe_rt_exit(code); } @@ -29,3 +33,21 @@ pub fn raw_free(p: *u8) -> void { // back. A test can insist the two agree; nothing else should care. pub fn allocs() -> i32 { unsafe { return fe_rt_allocs(); } } pub fn frees() -> i32 { unsafe { return fe_rt_frees(); } } + +pub fn raw_open(path: *u8, write: i32) -> i32 { + unsafe { return fe_rt_open(path, write); } +} + +pub fn raw_read(handle: i32, buf: *u8, len: usize) -> i32 { + unsafe { return fe_rt_read(handle, buf, len); } +} + +pub fn raw_close(handle: i32) -> void { + unsafe { fe_rt_close(handle); } +} + +/// The whole command line as one NUL-terminated string. Splitting it into +/// arguments is `std.io`'s job: the runtime should not know about quoting. +pub fn raw_cmdline() -> *u8 { + unsafe { return fe_rt_cmdline(); } +} diff --git a/fec/tests/exec/cmdargs.fe b/fec/tests/exec/cmdargs.fe new file mode 100644 index 0000000..4cb53ae --- /dev/null +++ b/fec/tests/exec/cmdargs.fe @@ -0,0 +1,21 @@ +// EXIT:0 +// OUTPUT:args ok +unit cmdargs; +import std.io; +import std.str; + +// The command line reaches the program. A compiler is told which file to read +// this way and no other. + +fn main() -> i32 { + var line: [512]u8 = undefined; + let n: usize = io.cmdline(line[..]); + let program: []u8 = io.arg(line[0..n], 0); + if program.n == 0 { @print("no program name\n"); return 1; } + if str.find(program, "cmdargs") == program.n { + @print("unexpected program name: {}\n", program); + return 2; + } + @print("args ok\n"); + return 0; +} diff --git a/fec/tests/exec/readfile.fe b/fec/tests/exec/readfile.fe new file mode 100644 index 0000000..a2888e0 --- /dev/null +++ b/fec/tests/exec/readfile.fe @@ -0,0 +1,37 @@ +// EXIT:0 +// OUTPUT:read 64 bytes +// OUTPUT:first line: // EXIT:0 +unit readfile; +import std.io; + +// Reads its own source and reports the first line. A program that can open a +// file is a program that can be a compiler. + +fn main() -> i32 { + var path: [64]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let n: usize = io.to_cstr(path[..], "fec/tests/exec/readfile.fe"); + let handle: i32 = io.open_read(path[0..n]) catch |e| { + @print("cannot open\n"); + return 1; + }; + var buf: [64]u8 = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]; + let got: usize = io.read(handle, buf[..]) catch |e| { + io.close(handle); + return 2; + }; + io.close(handle); + @print("read {} bytes\n", n); + var stop: usize = 0; + while stop < got { + if buf[stop] == 10 { break; } + stop = stop + 1; + } + @print("first line: {}\n", buf[0..stop]); + return 0; +} From 0fd3f187f439cd9b13d464ea06e00d5fd44b62c5 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:15:57 +0900 Subject: [PATCH 155/184] =?UTF-8?q?lower:=20=ED=83=9C=EA=B7=B8=EB=93=9C=20?= =?UTF-8?q?=EC=9C=A0=EB=8B=88=EC=98=A8=EC=9D=84=20=ED=95=B4=EC=B2=B4?= =?UTF-8?q?=ED=95=9C=EB=8B=A4=20--=20match=20=ED=8E=98=EC=9D=B4=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EC=99=80=20if=20let?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AST 도 IR 도 본질이 태그드 유니온이다. 태그 비교만 되고 안을 꺼내지 못하면 검사 없이 필드를 읽어야 하고, 그러면 안전성 이야기가 통째로 무너진다. 무언가를 담는 변이는 메모리다: 태그가 앞, 페이로드가 뒤. 태그를 읽는 것은 어느 쪽이든 같은 질문이고 자리만 다르다. 페이로드는 가리키지 않고 복사한다 -- 매치한 것의 소유권을 arm 이 가져가는 것이 보통이고, 그게 허용되는지는 검사기가 이미 판단했다. 레코드 변이의 필드 오프셋을 아무도 기록하지 않고 있었다. 레이아웃이 크기는 재면서 자리는 버렸다. Enum.Variant{...} 와 Enum.Variant(x) 와 페이로드 없는 Enum.Variant 를 모두 만든다. 마지막 것은 페이로드를 가진 enum 안에서는 태그만 든 값이다. some 7 / none / point 3 4 / empty exec.py 25/25. --- fec/src/lowerexp.c | 49 ++++++++++++++++++--- fec/src/lowerpri.h | 5 +++ fec/src/lowerprn.c | 21 +++++++++ fec/src/lowerstm.c | 89 +++++++++++++++++++++++++++++++++++++- fec/src/types.c | 5 +++ fec/tests/exec/patterns.fe | 35 +++++++++++++++ 6 files changed, 197 insertions(+), 7 deletions(-) create mode 100644 fec/tests/exec/patterns.fe diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c index e43cfc5..869a36f 100644 --- a/fec/src/lowerexp.c +++ b/fec/src/lowerexp.c @@ -108,13 +108,23 @@ Slot lower_expr_core(Lower *L, FeNode *n) fail(L, "this unary operator", n); return slot_void(); case FE_N_MEMBER: - /* A payload-free variant used as a value is just its tag. */ - if (t && t->kind == FE_TYPE_ENUM && !enum_has_payload(t) && - n->b && n->b->text) { + /* A variant used as a value carries nothing but its tag. When no + variant of the enum carries anything the whole value is that tag; + otherwise it is a tag sitting in front of an unused payload. */ + if (t && t->kind == FE_TYPE_ENUM && n->b && n->b->text) { FeVariantType *v = fe_type_variant(t, n->b->text); - if (v) + if (v && !enum_has_payload(t)) return slot_value(fe_ir_const(L->m, L->b, ir_type(t), (long)v->tag), ir_type(t)); + if (v && !v->field_count) { + unsigned local = scratch(L, t, "variant"); + unsigned tag = fe_ir_const(L->m, L->b, tag_type_of(t), + (long)v->tag); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag, + tag_type_of(t)); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, + ir_size(t)); + } } /* `error.Name` is a member of the open default set: a code, and nothing to look up. */ @@ -211,8 +221,37 @@ Slot lower_expr_core(Lower *L, FeNode *n) return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); } case FE_N_STRUCT_INIT: { - unsigned local = scratch(L, t, "struct"); + unsigned local; FeNode *f; + /* `Enum.Variant{ .. }` builds a variant, not a struct: the tag first, + then the named fields inside the payload area. */ + if (t && t->kind == FE_TYPE_ENUM && n->a && n->a->kind == FE_N_MEMBER) { + const FeVariantType *v = fe_type_variant(t, + n->a->b && n->a->b->text ? n->a->b->text : ""); + long base = (long)fe_type_payload_offset(t); + unsigned tag; + if (!v) { fail(L, "an unknown variant", n); return slot_void(); } + local = scratch(L, t, "variant"); + tag = fe_ir_const(L->m, L->b, tag_type_of(t), (long)v->tag); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag, + tag_type_of(t)); + for (f = n->children; f; f = f->next) { + unsigned i; + if (f->kind != FE_N_FIELD) continue; + for (i = 0; i < v->field_count; ++i) + if (f->text && v->fields[i].name && + !strcmp(v->fields[i].name, f->text)) break; + if (i == v->field_count) { + fail(L, "an unknown variant field", f); + return slot_void(); + } + store_into(L, fe_ir_at_local(local, + base + (long)v->fields[i].offset), + lower_expr(L, f->a), f, ir_size(v->fields[i].type)); + } + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, ir_size(t)); + } + local = scratch(L, t, "struct"); for (f = n->children; f; f = f->next) { FeFieldType *field; Slot v; diff --git a/fec/src/lowerpri.h b/fec/src/lowerpri.h index a72262b..5cb9c33 100644 --- a/fec/src/lowerpri.h +++ b/fec/src/lowerpri.h @@ -87,6 +87,11 @@ typedef struct Slot { #define SLICE_LEN_OFFSET 4L /* Every definition in lowering, so the split files can see each other. */ +FeIrType tag_type_of(const FeType *t); +void lower_if_let(Lower *L, FeNode *n); +unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n); +void bind_payload(Lower *L, Slot subject, const FeType *t, + const FeVariantType *v, FeNode *arm); void fail(Lower *L, const char *why, FeNode *n); FeIrType ir_type_of(const FeType *t); int enum_has_payload(const FeType *t); diff --git a/fec/src/lowerprn.c b/fec/src/lowerprn.c index eea3923..83f7ecb 100644 --- a/fec/src/lowerprn.c +++ b/fec/src/lowerprn.c @@ -160,6 +160,27 @@ Slot lower_call(Lower *L, FeNode *n) if (lower_mem(L, n, &built)) return built; if (lower_print(L, n, &built)) return built; } + /* `Enum.Variant(payload)` is a constructor, not a call. */ + if (ret && ret->kind == FE_TYPE_ENUM && n->a && n->a->kind == FE_N_MEMBER && + n->a->b && n->a->b->text) { + const FeVariantType *v = fe_type_variant(ret, n->a->b->text); + if (v) { + unsigned local = scratch(L, ret, "variant"); + unsigned tag = fe_ir_const(L->m, L->b, tag_type_of(ret), + (long)v->tag); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), tag, + tag_type_of(ret)); + if (v->field_count && n->children) + store_into(L, + fe_ir_at_local(local, + (long)fe_type_payload_offset(ret) + + (long)v->fields[0].offset), + lower_expr(L, n->children), n->children, + ir_size(v->fields[0].type)); + return slot_place(fe_ir_at_local(local, 0), FE_IR_MEM, + ir_size(ret)); + } + } if (!callee) { fail(L, "a call with no target", n); return slot_void(); } /* An aggregate result is written through a hidden first argument. */ if (rt == FE_IR_MEM) { diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index ff93d4d..527d04e 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -274,6 +274,81 @@ void lower_for(Lower *L, FeNode *n) arm's pattern in turn. The checker already proved the arms cover everything, so falling off the end cannot happen in a program that compiled -- but the generated code has to go somewhere, and going to the join is right. */ +/* The width of a tag: an enum's own, or the byte an optional puts in front. */ +FeIrType tag_type_of(const FeType *t) +{ + if (!t) return FE_IR_I8; + if (t->kind == FE_TYPE_ERROR_UNION) return FE_IR_I16; + if (t->kind == FE_TYPE_ENUM) return t->bits > 8U ? FE_IR_I16 : FE_IR_I8; + return FE_IR_I8; +} + +/* Give an arm's names somewhere to live and put the variant's payload there. + The payload is copied rather than pointed at: an arm that takes ownership of + what it matched is the normal case, and the checker has already decided + whether that was allowed. */ +void bind_payload(Lower *L, Slot subject, const FeType *t, + const FeVariantType *v, FeNode *arm) +{ + FeNode *name; + unsigned i; + long base; + if (!v || !v->field_count || !arm->children || !subject.is_place) return; + base = (long)fe_type_payload_offset(t); + name = arm->children; + for (i = 0; i < v->field_count && name; ++i, name = name->next) { + FeType *ft = v->fields[i].type; + unsigned local = declare_var(L, name->cname, ft, name->text); + FeIrPlace from = subject.place; + from.offset += base + (long)v->fields[i].offset; + store_into(L, fe_ir_at_local(local, 0), + slot_place(from, ir_type(ft), ir_size(ft)), name, + ir_size(ft)); + } +} + +/* `if let Some(x) = opt { .. } else { .. }` -- and its None twin. + + The optional is read once into a place, the tag decides the branch, and the + binding gets what was inside. A binding whose type is a reference gets the + address instead of a copy: the checker chose that when the payload was not + something you may quietly duplicate. */ +void lower_if_let(Lower *L, FeNode *n) +{ + FeType *opt = n->a ? n->a->sem_type : 0; + Slot value = lower_expr(L, n->a); + FeNode *binding = n->children; + int is_some = n->aux_text && !strcmp(n->aux_text, "Some"); + unsigned tag; + FeIrBlock *present; + FeIrBlock *absent; + FeIrBlock *join; + if (!value.is_place) { fail(L, "if let over a temporary", n); return; } + tag = wrapper_tag(L, value, opt, n); + present = new_block(L); + absent = new_block(L); + join = new_block(L); + fe_ir_br(L->b, tag, present->id, absent->id); + /* Which side runs the body depends on which pattern was written. */ + L->b = is_some ? present : absent; + if (is_some && binding) { + FeType *bt = binding->sem_type; + Slot payload = wrapper_payload(L, value, opt); + unsigned local = declare_var(L, binding->cname, bt, binding->text); + if (bt && (bt->kind == FE_TYPE_REF || bt->kind == FE_TYPE_RAW)) + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), + as_address(L, payload, n), FE_IR_PTR); + else + store_into(L, fe_ir_at_local(local, 0), payload, n, ir_size(bt)); + } + lower_stmt(L, n->b); + fe_ir_jmp(L->b, join->id); + L->b = is_some ? absent : present; + if (n->c) lower_stmt(L, n->c); + fe_ir_jmp(L->b, join->id); + L->b = join; +} + void lower_match(Lower *L, FeNode *n) { FeType *t = n->a ? n->a->sem_type : 0; @@ -282,8 +357,16 @@ void lower_match(Lower *L, FeNode *n) unsigned value; FeIrBlock *join; FeNode *arm; - if (it == FE_IR_MEM) { fail(L, "a match over a payload", n); return; } - value = as_value(L, subject, n->a); + /* A variant that carries something is memory: the tag comes first and the + payload after it. Reading the tag is then the same question either way, + just from a different place. */ + if (it == FE_IR_MEM) { + if (!subject.is_place) { fail(L, "a match over a temporary", n); return; } + it = tag_type_of(t); + value = fe_ir_load(L->m, L->b, it, subject.place); + } else { + value = as_value(L, subject, n->a); + } join = new_block(L); for (arm = n->children; arm; arm = arm->next) { FeIrBlock *body; @@ -307,6 +390,7 @@ void lower_match(Lower *L, FeNode *n) next = new_block(L); fe_ir_br(L->b, same, body->id, next->id); L->b = body; + bind_payload(L, subject, t, v, arm); lower_stmt(L, arm->a); fe_ir_jmp(L->b, join->id); L->b = next; @@ -362,6 +446,7 @@ void lower_stmt(Lower *L, FeNode *n) lower_return(L, n); return; case FE_N_IF: + if (n->text && !strcmp(n->text, "if let")) { lower_if_let(L, n); return; } lower_if(L, n); return; case FE_N_WHILE: diff --git a/fec/src/types.c b/fec/src/types.c index 4572476..1c1b707 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -443,6 +443,7 @@ unsigned long fe_type_payload_offset(const FeType *t) if (fe_m7_optional_uses_niche(t->elem)) return 0; return round_up(1UL, fe_type_align(t->elem)); } + if (t->kind == FE_TYPE_ENUM) return round_up(t->bits / 8U, t->align); return 0; } @@ -551,6 +552,10 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) layout_type(ctx, t->variants[i].fields[j].type); if (fe_type_align(t->variants[i].fields[j].type) > max_align) max_align = fe_type_align(t->variants[i].fields[j].type); + /* Where this field sits inside the payload area, which the + code generator needs and nobody was recording. */ + off = round_up(off, fe_type_align(t->variants[i].fields[j].type)); + t->variants[i].fields[j].offset = off; off += fe_type_size(t->variants[i].fields[j].type); } if (off > max_size) max_size = off; diff --git a/fec/tests/exec/patterns.fe b/fec/tests/exec/patterns.fe new file mode 100644 index 0000000..dfd7eba --- /dev/null +++ b/fec/tests/exec/patterns.fe @@ -0,0 +1,35 @@ +// EXIT:0 +// OUTPUT:some 7 +// OUTPUT:none +// OUTPUT:point 3 4 +// OUTPUT:empty +unit patterns; + +enum Shape { + Empty, + Point(i32), + Pair { x: i32, y: i32 }, +} + +fn pick(flag: bool) -> ?i32 { + if flag { return 7; } + return null; +} + +fn describe(s: Shape) -> void { + match s { + Empty => { @print("empty\n"); } + Point(v) => { @print("point {}\n", v); } + Pair { x, y } => { @print("point {} {}\n", x, y); } + } +} + +fn main() -> i32 { + if let Some(v) = pick(true) { @print("some {}\n", v); } + else { @print("unexpected\n"); } + if let None = pick(false) { @print("none\n"); } + else { @print("unexpected\n"); } + describe(Shape.Pair{ x: 3, y: 4 }); + describe(Shape.Empty); + return 0; +} From c06f5c50c4f676c65f490f47586be92badab4383 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:18:47 +0900 Subject: [PATCH 156/184] =?UTF-8?q?compiler:=20=EC=A1=B0=EC=9A=A9=ED=9E=88?= =?UTF-8?q?=20=EC=9E=98=EB=A6=AC=EB=8D=98=20=EC=83=81=ED=95=9C=EC=9D=84=20?= =?UTF-8?q?=EC=97=86=EC=95=A4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lowering 의 변수·정리 목록·오류 이름 세 배열은 넘치면 오류가 아니라 넘친 것을 버리고 틀린 코드를 냈다. 한계에 닿는 방식 중 최악이다. 이제 자란다. 코드 생성기는 지역이 512개를 넘으면 함수를 아예 방출하지 않고 지나갔다. 이제 지역 수만큼 자리를 잡는다. 유닛 64 -> 256, 제네릭 인스턴스 512 -> 4096. 둘 다 원래 보고는 했지만 장난감을 기준으로 고른 숫자였다. 209 -> 213 fixture, exec 25/25. --- fec/src/check.h | 2 +- fec/src/lower.c | 28 +++++++++++++++++++++++++--- fec/src/lowerpri.h | 18 ++++++++++++++---- fec/src/lowerstm.c | 21 ++++++++++++++++----- fec/src/resolve.h | 2 +- fec/src/x86.c | 10 ++++++++-- 6 files changed, 65 insertions(+), 16 deletions(-) diff --git a/fec/src/check.h b/fec/src/check.h index 59bde60..7d55ba7 100644 --- a/fec/src/check.h +++ b/fec/src/check.h @@ -11,7 +11,7 @@ typedef struct FeScope FeScope; spelling of its type arguments (SPEC 9). The table both deduplicates requests and bounds how long a chain of new ones can get. */ #define FE_GENERIC_KEY_MAX 320 -#define FE_GENERIC_INSTANCE_MAX 512 +#define FE_GENERIC_INSTANCE_MAX 4096 typedef struct FeInstance { char key[FE_GENERIC_KEY_MAX]; /* What lowering needs to build this instance's code: the declaration, the diff --git a/fec/src/lower.c b/fec/src/lower.c index 045197e..80d13f6 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -118,18 +118,37 @@ int needs_release(const FeType *t) return t->has_drop != 0; } +int lower_reserve(Lower *L, void **items, unsigned *capacity, unsigned needed, + unsigned long item_size) +{ + unsigned want; + void *grown; + if (needed < *capacity) return 1; + want = *capacity ? *capacity * 2U : 16U; + while (want <= needed) want *= 2U; + grown = fe_arena_alloc(&L->m->arena, (size_t)(want * item_size)); + if (!grown) { fail(L, "a function this large", 0); return 0; } + if (*items) memcpy(grown, *items, (size_t)(*capacity * item_size)); + *items = grown; + *capacity = want; + return 1; +} + unsigned declare_var(Lower *L, const char *cname, const FeType *t, const char *name) { unsigned local = fe_ir_local(L->m, L->fn, ir_type(t), ir_size(t), ir_align(t), name); - if (L->var_count < LOWER_MAX_LOCALS) { + if (lower_reserve(L, (void **)&L->vars, &L->var_capacity, L->var_count, + (unsigned long)sizeof(LowerVar))) { L->vars[L->var_count].cname = cname; L->vars[L->var_count].local = local; L->vars[L->var_count].by_address = 0; ++L->var_count; } - if (needs_release(t) && L->owed_count < 64) { + if (needs_release(t) && + lower_reserve(L, (void **)&L->owed, &L->owed_capacity, L->owed_count, + (unsigned long)sizeof *L->owed)) { unsigned flag = fe_ir_local(L->m, L->fn, FE_IR_I8, 1, 1, "live"); unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); fe_ir_store(L->m, L->b, fe_ir_at_local(flag, 0), zero, FE_IR_I8); @@ -230,7 +249,10 @@ void note_error_name(Lower *L, const char *name) { unsigned i; unsigned at; - if (!name || L->error_count >= 256) return; + if (!name) return; + if (!lower_reserve(L, (void **)&L->error_names, &L->error_capacity, + L->error_count, (unsigned long)sizeof(const char *))) + return; for (i = 0; i < L->error_count; ++i) if (!strcmp(L->error_names[i], name)) return; /* Kept sorted as it is built, so the numbering is the spelling order. */ diff --git a/fec/src/lowerpri.h b/fec/src/lowerpri.h index 5cb9c33..b46c556 100644 --- a/fec/src/lowerpri.h +++ b/fec/src/lowerpri.h @@ -24,7 +24,6 @@ * register and an aggregate does not fit in one. * ------------------------------------------------------------------------- */ -#define LOWER_MAX_LOCALS 256 typedef struct LowerVar { const char *cname; @@ -41,8 +40,12 @@ typedef struct Lower { FeIrBlock *b; /* the block being appended to */ FeType *ret_type; unsigned ret_local; /* hidden result address, when returning mem */ - LowerVar vars[LOWER_MAX_LOCALS]; + /* These three grow. A fixed size here does not report a program that is + too big -- it quietly drops what does not fit and generates wrong code, + which is the worst way for a limit to be reached. */ + LowerVar *vars; unsigned var_count; + unsigned var_capacity; /* Loop targets, for break and continue. */ unsigned break_target[32]; unsigned continue_target[32]; @@ -60,14 +63,16 @@ typedef struct Lower { unsigned local; /* the owned value, otherwise */ unsigned flag; FeType *type; - } owed[64]; + } *owed; unsigned owed_count; + unsigned owed_capacity; /* Every `error.Name` used anywhere in the build, sorted, numbered from one. SPEC 4.6: the names are collected rather than declared, and the order is fixed by the spelling so that the same program always gets the same codes however the build was ordered. */ - const char *error_names[256]; + const char **error_names; unsigned error_count; + unsigned error_capacity; int failed; } Lower; @@ -86,6 +91,11 @@ typedef struct Slot { #define SLICE_PTR_OFFSET 0L #define SLICE_LEN_OFFSET 4L +/* Grow one of the checker's own arrays. Returns zero when there is no more + memory, which the caller reports rather than ignores. */ +int lower_reserve(Lower *L, void **items, unsigned *capacity, unsigned needed, + unsigned long item_size); + /* Every definition in lowering, so the split files can see each other. */ FeIrType tag_type_of(const FeType *t); void lower_if_let(Lower *L, FeNode *n); diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index 527d04e..7591523 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -198,21 +198,30 @@ void lower_for(Lower *L, FeNode *n) with one it is the element. */ counter = fe_ir_local(L->m, L->fn, FE_IR_I32, 4, 4, "index"); if (n->aux_cname) { + (void)lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, + (unsigned long)sizeof(LowerVar)); L->vars[L->var_count].cname = n->cname; L->vars[L->var_count].local = counter; L->vars[L->var_count].by_address = 0; - if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + ++L->var_count; item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->aux_text); + (void)lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, + (unsigned long)sizeof(LowerVar)); L->vars[L->var_count].cname = n->aux_cname; L->vars[L->var_count].local = item; L->vars[L->var_count].by_address = 0; - if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + ++L->var_count; } else { item = fe_ir_local(L->m, L->fn, FE_IR_PTR, 4, 4, n->text); + (void)lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, + (unsigned long)sizeof(LowerVar)); L->vars[L->var_count].cname = n->cname; L->vars[L->var_count].local = item; L->vars[L->var_count].by_address = 0; - if (L->var_count < LOWER_MAX_LOCALS) ++L->var_count; + ++L->var_count; } { unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I32, 0); @@ -463,7 +472,8 @@ void lower_stmt(Lower *L, FeNode *n) lower_stmt(L, n->a); return; case FE_N_DEFER: - if (L->owed_count < 64) { + if (lower_reserve(L, (void **)&L->owed, &L->owed_capacity, + L->owed_count, (unsigned long)sizeof *L->owed)) { L->owed[L->owed_count].block = n->a; L->owed[L->owed_count].local = 0; L->owed[L->owed_count].flag = 0; @@ -543,7 +553,8 @@ void lower_fn_as(Lower *L, FeNode *fn, const char *name) ? fe_ir_local(L->m, f, FE_IR_PTR, 4, 4, p->text) : fe_ir_local(L->m, f, ir_type(pt), ir_size(pt), ir_align(pt), p->text); - if (L->var_count < LOWER_MAX_LOCALS) { + if (lower_reserve(L, (void **)&L->vars, &L->var_capacity, + L->var_count, (unsigned long)sizeof(LowerVar))) { L->vars[L->var_count].cname = p->cname; L->vars[L->var_count].local = local; L->vars[L->var_count].by_address = by_address; diff --git a/fec/src/resolve.h b/fec/src/resolve.h index e859093..69448c1 100644 --- a/fec/src/resolve.h +++ b/fec/src/resolve.h @@ -15,7 +15,7 @@ what makes a unit path map to a FAT/DOS 8.3 source path unambiguously. */ #define FE_UNIT_SEGMENT_MAX 8 #define FE_UNIT_PATH_MAX 128 -#define FE_BUILD_UNIT_MAX 64 +#define FE_BUILD_UNIT_MAX 256 typedef struct FeUnit { char name[FE_UNIT_PATH_MAX]; /* canonical dotted path */ diff --git a/fec/src/x86.c b/fec/src/x86.c index 065e507..9ddd64b 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -1,5 +1,6 @@ #include "x86.h" #include +#include /* ------------------------------------------------------------------------- * * i386 code generation @@ -269,13 +270,17 @@ static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) { Frame fr; - long storage[512]; + long *storage; const FeIrBlock *b; const FeIrValue *v; unsigned i; long arg = 8; if (f->is_extern || !f->first) return; - if (f->local_count > 512) return; + /* One offset per local, however many there are. A fixed array here would + silently stop emitting a function that had too many. */ + storage = (long *)malloc((size_t)(f->local_count ? f->local_count : 1) * + sizeof(long)); + if (!storage) return; frame_layout(&fr, f, storage); fprintf(out, "\npublic %s\n", f->name); @@ -322,6 +327,7 @@ static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) } } fprintf(out, "%s endp\n", f->name); + free(storage); (void)m; } From ae83f5b1423ace1e28f2d0a5223e132a71c386f2 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:21:40 +0900 Subject: [PATCH 157/184] =?UTF-8?q?std:=20mem.Arena=20=EB=A5=BC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPEC R11 은 재귀·그래프 모양 데이터를 아레나가 값을 소유하고 정수 핸들이 가리키는 것으로 답한다. 그 답은 아레나가 실제로 있어야 쓸 수 있다. 핸들은 오프셋이라 아레나가 사는 동안 유효하고, 두 핸들을 비교하는 것은 두 수를 비교하는 것이다. 아레나는 몰래 자라지 않는다 -- 움직인 핸들은 더 이상 아무것도 가리키지 않기 때문이다. 길에서 고친 것 셋: - Self 가 제네릭 인스턴스에서만 타입으로 묶여 있어서, 평범한 구조체의 Self{..} 가 안 풀렸다. 이제 모든 메서드에서 묶는다. - binding.Type.method() 가 식 자리에서 해석되지 않았다. - 다른 유닛의 비제네릭 구조체 메서드가 lowering 되지 않고 extern 으로만 나갔다. 파일을 나눌 때 그 가지가 빠졌다. handles 0 4 8 / value 65 / full / reset 0 / balanced exec.py 26/26. --- fec/src/checkgen.c | 10 ++++++ fec/src/checkstm.c | 11 +++++++ fec/src/lowerpri.h | 1 + fec/src/lowerstm.c | 13 ++++++++ fec/std/mem.fe | 67 +++++++++++++++++++++++++++++++++++------ fec/tests/exec/arena.fe | 39 ++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 fec/tests/exec/arena.fe diff --git a/fec/src/checkgen.c b/fec/src/checkgen.c index 9be5059..45c1a31 100644 --- a/fec/src/checkgen.c +++ b/fec/src/checkgen.c @@ -358,6 +358,16 @@ FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) if (t && t->kind!=FE_TYPE_UNKNOWN) { *ok=1; return t; } return unknown(c); } + /* `binding.Name` names a type in another unit. */ + if (n->kind==FE_N_MEMBER && n->a && n->a->kind==FE_N_IDENT && + n->b && n->b->text) { + FeUnit *bound=binding_unit(s,n->a); + if (bound) { + FeType *there=unit_type(c,bound,n->b->text); + if (there) { *ok=1; return there; } + } + return unknown(c); + } if (n->kind==FE_N_CALL && n->a && (n->a->kind==FE_N_IDENT || (n->a->kind==FE_N_MEMBER && n->a->a && diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index b3b1e0a..458bd1c 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -504,6 +504,16 @@ void check_method(FeCheck *c, FeNode *fn, FeScope *globals, FeCheckerState s; FeNode *x; FeType *t; + FeBindSave self_save; + /* `Self` names the type a method belongs to, wherever it appears -- in a + signature, and in `Self{ .. }`. Binding it as a type makes both work the + same way, and the same way a generic instance already worked. */ + self_save.count=c->types.param_count; + { + unsigned i; + for(i=0;itypes.params[i]; + } + bind_self(c,owner); s.c=c; s.globals=globals; s.scope=scope_new(&s,globals); @@ -521,6 +531,7 @@ void check_method(FeCheck *c, FeNode *fn, FeScope *globals, local_cname(c,x->text ? x->text : "arg"),x); } if(fn->c) check_stmt(&s,fn->c); + pop_bindings(c,&self_save); } int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) diff --git a/fec/src/lowerpri.h b/fec/src/lowerpri.h index b46c556..3c5522d 100644 --- a/fec/src/lowerpri.h +++ b/fec/src/lowerpri.h @@ -98,6 +98,7 @@ int lower_reserve(Lower *L, void **items, unsigned *capacity, unsigned needed, /* Every definition in lowering, so the split files can see each other. */ FeIrType tag_type_of(const FeType *t); +int struct_is_generic(const FeNode *decl); void lower_if_let(Lower *L, FeNode *n); unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n); void bind_payload(Lower *L, Slot subject, const FeType *t, diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index 7591523..a9dff93 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -515,6 +515,12 @@ void lower_global(Lower *L, FeNode *n) fe_ir_global(L->m, n->cname, ir_type(t), size, ir_align(t), init); } +/* A declaration with type parameters is a pattern, not code. */ +int struct_is_generic(const FeNode *decl) +{ + return decl && decl->a && decl->a->children != 0; +} + int fn_is_generic(const FeNode *fn) { FeNode *p; @@ -594,6 +600,13 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) lower_global(&L, n); + else if (n->kind == FE_N_STRUCT && !struct_is_generic(n)) { + /* A method is a function whose first parameter is the value it + was reached through; the storage is the same either way. */ + FeNode *m; + for (m = n->children; m; m = m->next) + if (m->kind == FE_N_FN && m->c) lower_fn(&L, m); + } else if (n->kind == FE_N_FN && !n->c) { /* A declaration with no body is something the linker will find: the runtime, or a C library. */ diff --git a/fec/std/mem.fe b/fec/std/mem.fe index 4fe0cd7..f4524f7 100644 --- a/fec/std/mem.fe +++ b/fec/std/mem.fe @@ -1,13 +1,60 @@ -unit mem; +unit std.mem; +import std.sys; -pub fn create(value: T) -> !^T; -pub fn destroy(p: *void); -pub fn alloc_slice(T: type, n: usize) -> !^[]T; -pub fn replace(dst: &mut T, value: T) -> T; -pub fn copy(dst: []mut u8, src: []u8); +// `create`, `destroy`, `alloc_slice` and `replace` are compiler intrinsics: +// they need to know the type they are handed, which no signature can say. +// What is written here is what can be written in Ferro. + +/// A block of storage handed out in pieces, released all at once. +/// +/// SPEC R11 answers recursive and graph-shaped data with an arena that owns +/// the values and integer handles that reference them. This is that arena. A +/// handle is an offset, so it stays valid while the arena does, and comparing +/// two handles is comparing two numbers. pub struct Arena { - ptr: *void, - pub fn init() -> Arena { return Arena{ ptr: null }; } - pub fn reset(self: &mut Self) { } - pub fn drop(self: &mut Self) { } + bytes: ^[]mut u8, + used: usize, + + pub fn with_capacity(n: usize) -> !Self { + let room: ^[]mut u8 = try mem.alloc_slice(u8, n); + return Self{ bytes: room, used: 0 }; + } + + pub fn size(self: &Self) -> usize { return self.used; } + + pub fn room(self: &Self) -> usize { return self.bytes.^.n; } + + /// Reserve `n` bytes aligned to `align` and give back where they start. + /// Failure is running out of room, which the caller decides what to do + /// about; the arena never grows behind your back, because a handle that + /// moved would no longer mean anything. + pub fn alloc(self: &mut Self, n: usize, align: usize) -> !usize { + var at: usize = self.used; + if align > 1 { + let over: usize = at % align; + if over != 0 { at = at + align - over; } + } + if at + n > self.bytes.^.n { return error.ArenaFull; } + self.used = at + n; + return at; + } + + /// One byte at a handle. Reading and writing go through here so that a + /// handle can be checked once, in one place. + pub fn at(self: &Self, handle: usize) -> !u8 { + if handle >= self.used { return error.BadHandle; } + return self.bytes.^[handle]; + } + + pub fn put(self: &mut Self, handle: usize, value: u8) -> !void { + if handle >= self.used { return error.BadHandle; } + self.bytes.^[handle] = value; + return; + } + + /// Forget everything handed out so far. Every handle from before is stale; + /// that is the trade an arena makes. + pub fn reset(self: &mut Self) -> void { self.used = 0; } + + pub fn drop(self: &mut Self) -> void { mem.destroy(self.bytes); } } diff --git a/fec/tests/exec/arena.fe b/fec/tests/exec/arena.fe new file mode 100644 index 0000000..88da9b7 --- /dev/null +++ b/fec/tests/exec/arena.fe @@ -0,0 +1,39 @@ +// EXIT:0 +// OUTPUT:handles 0 4 8 +// OUTPUT:value 65 +// OUTPUT:full +// OUTPUT:reset 0 +// OUTPUT:balanced +unit arena; +import std.io; +import std.mem; +import std.sys; + +// SPEC R11: recursive and graph-shaped data is answered by an arena that owns +// the values and integer handles that point into it. This is that shape. + +fn run() -> !void { + var a: mem.Arena = try mem.Arena.with_capacity(16); + let first: usize = try a.alloc(4, 4); + let second: usize = try a.alloc(4, 4); + let third: usize = try a.alloc(4, 4); + @print("handles {} {} {}\n", first, second, third); + try a.put(first, 65); + let got: u8 = try a.at(first); + @print("value {}\n", got); + let over: usize = a.alloc(64, 1) catch |e| { + @print("full\n"); + a.reset(); + @print("reset {}\n", a.size()); + return; + }; + @print("unexpected room {}\n", over); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() != sys.frees() { @print("leaked\n"); return 2; } + @print("balanced\n"); + return 0; +} From 4fe0073365de3ad21533ffd089f9ae7732c84aec Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:30:22 +0900 Subject: [PATCH 158/184] =?UTF-8?q?backend:=20=EB=A0=88=EC=A7=80=EC=8A=A4?= =?UTF-8?q?=ED=84=B0=EB=A5=BC=20=ED=95=A0=EB=8B=B9=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 임시값마다 스택 슬롯을 주던 것을 바꿨다. ebx, esi, edi 세 개를 나눠준다 -- eax/ecx/edx 는 이 방출기가 계산하는 자리이고, 남는 셋은 호출을 저장 없이 넘긴다. IR 은 '임시값은 블록을 넘지 않는다'로 설계했지만 lowering 이 실제로는 넘기는 것을 만든다 -- 경계 검사가 인덱스를 계산한 자리와 쓰는 자리 사이에서 블록을 쪼갠다. 그래서 자격은 함수 전체를 보고 정하고, 블록 안의 선형 스캔은 살아남은 것만 다룬다. 그리고 목적지 레지스터에 직접 계산한다. 상수, 적재, 주소, 그리고 전폭 산술과 비교가 스크래치 레지스터를 거치지 않는다. 좁은 연산은 여전히 eax 를 거친다 -- esi 와 edi 에는 바이트 반쪽이 없다. 버그 둘: - 프롤로그에 ebx 를 넣는 편집이 copy 의 push 까지 같이 바꿔서 pop 없는 push 가 생겼다. 스택이 어긋나 17개가 죽었다. - load_place_base 가 load_temp 을 거치지 않고 슬롯을 직접 읽었다. 레지스터에 있는 포인터를 쓰지 않은 메모리에서 읽었다. calc 4437 -> 3796 줄 (-14%) wordfreq 6023 -> 5348 줄 214/214, 26/26. --- fec/src/x86.c | 276 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 253 insertions(+), 23 deletions(-) diff --git a/fec/src/x86.c b/fec/src/x86.c index 9ddd64b..bec88fa 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -18,11 +18,20 @@ * parameter and a local are the same thing to everything below. * ------------------------------------------------------------------------- */ +/* Which register a temporary lives in, or none. Only ebx, esi and edi are + handed out: eax, ecx and edx are the scratch this emitter computes in, and + the three that are left survive a call without being saved. */ +#define REG_NONE 0 +#define REG_COUNT 3 +static const char *const REGS[REG_COUNT] = { "ebx", "esi", "edi" }; + typedef struct Frame { const FeIrFunc *f; long *local_off; /* [ebp + off] for each local */ long temp_base; /* first temporary slot */ long size; /* bytes to subtract from esp */ + /* 0 means the temporary lives in its stack slot. */ + unsigned char *temp_reg; } Frame; static long align_up(long v, long a) @@ -67,6 +76,120 @@ static void frame_layout(Frame *fr, const FeIrFunc *f, long *storage) fr->size = align_up(off, 4); } +/* Which temporaries a value reads. Returns how many it wrote into `used`. */ +static unsigned reads_of(const FeIrValue *v, unsigned *used) +{ + unsigned n = 0; + unsigned i; + switch (v->op) { + case FE_IR_CONST: break; + case FE_IR_LOAD: + case FE_IR_ADDR: + if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; + break; + case FE_IR_STORE: + if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; + used[n++] = v->a; + break; + case FE_IR_COPY: + if (v->place.base == FE_PLACE_TEMP) used[n++] = v->place.index; + if (v->place2.base == FE_PLACE_TEMP) used[n++] = v->place2.index; + break; + case FE_IR_CAST: + used[n++] = v->a; + break; + case FE_IR_CALL: + for (i = 0; i < v->arg_count && n < 18; ++i) used[n++] = v->args[i]; + break; + default: + used[n++] = v->a; + used[n++] = v->b; + break; + } + return n; +} + +/* Give registers to the temporaries that can hold one. + + A temporary that is defined in one block and read in another has to go + through memory: this walks one block at a time and knows nothing about the + others. Lowering does produce such temporaries -- a bounds check splits a + block between computing an index and using it -- so eligibility is decided + over the whole function first, and the scan inside a block only considers + what survived that. */ +static void allocate_registers(Frame *fr, const FeIrFunc *f) +{ + unsigned n = f->temp_count; + unsigned char *single; /* 1 while the temporary stays in one block */ + unsigned *home; /* the block it was defined in */ + unsigned *last; /* the last instruction in that block to read it */ + const FeIrBlock *b; + const FeIrValue *v; + unsigned used[20]; + unsigned i, k, at; + if (!n) { fr->temp_reg = 0; return; } + fr->temp_reg = (unsigned char *)calloc(n, 1); + single = (unsigned char *)calloc(n, 1); + home = (unsigned *)calloc(n, sizeof(unsigned)); + last = (unsigned *)calloc(n, sizeof(unsigned)); + if (!fr->temp_reg || !single || !home || !last) { + free(single); free(home); free(last); + return; + } + for (i = 0; i < n; ++i) { single[i] = 1; home[i] = 0xFFFFFFFFU; } + for (b = f->first; b; b = b->next) { + for (v = b->first; v; v = v->next) { + if (v->has_dest) { + if (home[v->dest] != 0xFFFFFFFFU) single[v->dest] = 0; + home[v->dest] = b->id; + } + k = reads_of(v, used); + for (i = 0; i < k; ++i) + if (used[i] < n && home[used[i]] != b->id) single[used[i]] = 0; + } + if (b->term == FE_IR_BR && b->cond < n && home[b->cond] != b->id) + single[b->cond] = 0; + if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n && + home[b->ret_value] != b->id) + single[b->ret_value] = 0; + } + + for (b = f->first; b; b = b->next) { + unsigned char busy[REG_COUNT]; + unsigned owner[REG_COUNT]; + for (i = 0; i < REG_COUNT; ++i) { busy[i] = 0; owner[i] = 0; } + /* When each temporary is last read in this block. */ + at = 0; + for (v = b->first; v; v = v->next, ++at) { + k = reads_of(v, used); + for (i = 0; i < k; ++i) + if (used[i] < n && single[used[i]]) last[used[i]] = at; + } + if (b->term == FE_IR_BR && b->cond < n && single[b->cond]) + last[b->cond] = at; + if (b->term == FE_IR_RET && b->has_ret_value && b->ret_value < n && + single[b->ret_value]) last[b->ret_value] = at; + + at = 0; + for (v = b->first; v; v = v->next, ++at) { + /* Free whatever was read for the last time before this. */ + for (i = 0; i < REG_COUNT; ++i) + if (busy[i] && last[owner[i]] < at) busy[i] = 0; + if (!v->has_dest || !single[v->dest]) continue; + /* A call clobbers the scratch registers but not these three, so a + result can still be kept in one across the call that made it. */ + for (i = 0; i < REG_COUNT; ++i) + if (!busy[i]) { + busy[i] = 1; + owner[i] = v->dest; + fr->temp_reg[v->dest] = (unsigned char)(i + 1); + break; + } + } + } + free(single); free(home); free(last); +} + static long temp_off(const Frame *fr, unsigned t) { return fr->temp_base - (long)(t + 1) * TEMP_SLOT; @@ -111,22 +234,68 @@ static void place_addr(const Frame *fr, const FeIrPlace *p, char *buf) } /* A temporary-based place needs its pointer in a register first. */ +static void load_temp(const Frame *fr, unsigned t, const char *reg, + FILE *out); + static void load_place_base(const Frame *fr, const FeIrPlace *p, FILE *out) { if (p->base != FE_PLACE_TEMP) return; - fprintf(out, " mov edx, [ebp%+ld]\n", temp_off(fr, p->index)); + /* Through load_temp, not straight from the slot: the pointer may be living + in a register, in which case the slot was never written. */ + load_temp(fr, p->index, "edx", out); } static void load_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) { + if (fr->temp_reg && fr->temp_reg[t]) { + const char *from = REGS[fr->temp_reg[t] - 1]; + if (strcmp(from, reg) != 0) + fprintf(out, " mov %s, %s\n", reg, from); + return; + } fprintf(out, " mov %s, [ebp%+ld]\n", reg, temp_off(fr, t)); } static void store_temp(const Frame *fr, unsigned t, const char *reg, FILE *out) { + if (fr->temp_reg && fr->temp_reg[t]) { + const char *to = REGS[fr->temp_reg[t] - 1]; + if (strcmp(to, reg) != 0) + fprintf(out, " mov %s, %s\n", to, reg); + return; + } fprintf(out, " mov [ebp%+ld], %s\n", temp_off(fr, t), reg); } +/* The register a temporary lives in, or null when it lives in its slot. */ +static const char *reg_home(const Frame *fr, unsigned t) +{ + if (!fr->temp_reg || !fr->temp_reg[t]) return 0; + return REGS[fr->temp_reg[t] - 1]; +} + +/* Something an instruction can take as its right-hand operand: a register, or + the temporary's slot read in place. */ +static void operand_of(const Frame *fr, unsigned t, char *buf) +{ + const char *r = reg_home(fr, t); + if (r) strcpy(buf, r); + else sprintf(buf, "dword ptr [ebp%+ld]", temp_off(fr, t)); +} + +static const char *simple_op(FeIrOp op) +{ + switch (op) { + case FE_IR_ADD: return "add "; + case FE_IR_SUB: return "sub "; + case FE_IR_AND: return "and "; + case FE_IR_OR: return "or "; + case FE_IR_XOR: return "xor "; + case FE_IR_MUL: return "imul"; + default: return 0; + } +} + static const char *cmp_set(FeIrOp op, int is_unsigned) { switch (op) { @@ -146,6 +315,39 @@ static void emit_binary(const Frame *fr, const FeIrValue *v, FILE *out) FeIrType t = is_cmp ? (FeIrType)v->imm : v->type; const char *a = reg_of(t, 0); const char *c = reg_of(t, 1); + /* When the result has a register of its own and the operation is one that + can work on any register, the whole thing happens there: no trip through + the scratch register and no trip through memory. + + Only the full-width operations qualify. esi and edi have no byte halves, + so a narrow operation still goes through eax, where they do. */ + if (!is_cmp && v->has_dest && (t == FE_IR_I32 || t == FE_IR_PTR) && + simple_op(v->op)) { + const char *d = reg_home(fr, v->dest); + const char *rb = reg_home(fr, v->b); + if (d && !(rb && strcmp(rb, d) == 0)) { + char right[64]; + load_temp(fr, v->a, d, out); + operand_of(fr, v->b, right); + fprintf(out, " %s %s, %s\n", simple_op(v->op), d, right); + return; + } + } + /* A full-width comparison can read both sides where they already are; the + answer still has to come out of `al`, which is why it lands in eax when + the result has no register of its own. */ + if (is_cmp && (t == FE_IR_I32 || t == FE_IR_PTR)) { + const char *left = reg_home(fr, v->a); + const char *d = reg_home(fr, v->dest); + char right[64]; + if (!left) { load_temp(fr, v->a, "eax", out); left = "eax"; } + operand_of(fr, v->b, right); + fprintf(out, " cmp %s, %s\n", left, right); + fprintf(out, " %s al\n", cmp_set(v->op, v->is_unsigned)); + fprintf(out, " movzx %s, al\n", d ? d : "eax"); + if (!d) store_temp(fr, v->dest, "eax", out); + return; + } load_temp(fr, v->a, "eax", out); load_temp(fr, v->b, "ecx", out); if (is_cmp) { @@ -186,34 +388,50 @@ static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) char addr[128]; unsigned i; switch (v->op) { - case FE_IR_CONST: - fprintf(out, " mov eax, %ld\n", v->imm); - store_temp(fr, v->dest, "eax", out); + case FE_IR_CONST: { + const char *d = reg_home(fr, v->dest); + fprintf(out, " mov %s, %ld\n", d ? d : "eax", v->imm); + if (!d) store_temp(fr, v->dest, "eax", out); break; - case FE_IR_LOAD: + } + case FE_IR_LOAD: { + const char *d = reg_home(fr, v->dest); + const char *into = d ? d : "eax"; load_place_base(fr, &v->place, out); place_addr(fr, &v->place, addr); if (v->type == FE_IR_I8) - fprintf(out, " movzx eax, byte ptr %s\n", addr); + fprintf(out, " movzx %s, byte ptr %s\n", into, addr); else if (v->type == FE_IR_I16) - fprintf(out, " movzx eax, word ptr %s\n", addr); + fprintf(out, " movzx %s, word ptr %s\n", into, addr); else - fprintf(out, " mov eax, dword ptr %s\n", addr); - store_temp(fr, v->dest, "eax", out); + fprintf(out, " mov %s, dword ptr %s\n", into, addr); + if (!d) store_temp(fr, v->dest, "eax", out); break; - case FE_IR_STORE: + } + case FE_IR_STORE: { + const char *from = reg_home(fr, v->a); load_place_base(fr, &v->place, out); place_addr(fr, &v->place, addr); + /* A full-width value already in a register goes straight out; a narrow + one needs a byte or word half, which only eax has here. */ + if (from && (v->type == FE_IR_I32 || v->type == FE_IR_PTR)) { + fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, + from); + break; + } load_temp(fr, v->a, "eax", out); fprintf(out, " mov %s %s, %s\n", word_of(v->type), addr, reg_of(v->type, 0)); break; - case FE_IR_ADDR: + } + case FE_IR_ADDR: { + const char *d = reg_home(fr, v->dest); load_place_base(fr, &v->place, out); place_addr(fr, &v->place, addr); - fprintf(out, " lea eax, %s\n", addr); - store_temp(fr, v->dest, "eax", out); + fprintf(out, " lea %s, %s\n", d ? d : "eax", addr); + if (!d) store_temp(fr, v->dest, "eax", out); break; + } case FE_IR_CAST: load_temp(fr, v->a, "eax", out); /* Narrowing is free once everything is kept in a 32-bit slot; widening @@ -240,25 +458,33 @@ static void emit_value(const Frame *fr, const FeIrValue *v, FILE *out) case FE_IR_COPY: { char dst[128]; char src[128]; - /* The source base and the destination base both want edx, so a - temporary-based place is resolved into esi or edi first. */ + /* Both addresses are worked out in the scratch registers first, and + only then does the block copy take over esi and edi -- which may be + holding temporaries, so it hands them back. */ if (v->place2.base == FE_PLACE_TEMP) { - load_temp(fr, v->place2.index, "esi", out); - sprintf(src, "[esi%+ld]", v->place2.offset); + load_temp(fr, v->place2.index, "eax", out); + if (v->place2.offset) + fprintf(out, " add eax, %ld\n", v->place2.offset); } else { place_addr(fr, &v->place2, src); + fprintf(out, " lea eax, %s\n", src); } if (v->place.base == FE_PLACE_TEMP) { - load_temp(fr, v->place.index, "edi", out); - sprintf(dst, "[edi%+ld]", v->place.offset); + load_temp(fr, v->place.index, "edx", out); + if (v->place.offset) + fprintf(out, " add edx, %ld\n", v->place.offset); } else { place_addr(fr, &v->place, dst); + fprintf(out, " lea edx, %s\n", dst); } - fprintf(out, " lea esi, %s\n", src); - fprintf(out, " lea edi, %s\n", dst); + fprintf(out, " push esi\n"); + fprintf(out, " push edi\n"); + fprintf(out, " mov esi, eax\n"); + fprintf(out, " mov edi, edx\n"); fprintf(out, " mov ecx, %ld\n", v->imm); fprintf(out, " cld\n"); fprintf(out, " rep movsb\n"); + fprintf(out, " pop edi\n pop esi\n"); break; } default: @@ -282,13 +508,15 @@ static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) sizeof(long)); if (!storage) return; frame_layout(&fr, f, storage); + allocate_registers(&fr, f); fprintf(out, "\npublic %s\n", f->name); fprintf(out, "%s proc near\n", f->name); fprintf(out, " push ebp\n"); fprintf(out, " mov ebp, esp\n"); if (fr.size) fprintf(out, " sub esp, %ld\n", fr.size); - fprintf(out, " push esi\n push edi\n"); + fprintf(out, " push ebx\n push esi\n" + " push edi\n"); /* Copy the incoming arguments into the frame. */ for (i = 0; i < f->param_count; ++i) { fprintf(out, " mov eax, [ebp+%ld]\n", arg); @@ -313,7 +541,8 @@ static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) break; case FE_IR_RET: if (b->has_ret_value) load_temp(&fr, b->ret_value, "eax", out); - fprintf(out, " pop edi\n pop esi\n"); + fprintf(out, " pop edi\n pop esi\n" + " pop ebx\n"); fprintf(out, " mov esp, ebp\n pop ebp\n"); fprintf(out, " ret\n"); break; @@ -328,6 +557,7 @@ static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) } fprintf(out, "%s endp\n", f->name); free(storage); + free(fr.temp_reg); (void)m; } From 76cb7e254c7063e75420b6dec94fb03e5677dc36 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:35:21 +0900 Subject: [PATCH 159/184] =?UTF-8?q?lexer:=20Ferro=20=EC=9D=98=20=EB=A0=89?= =?UTF-8?q?=EC=84=9C=EB=A5=BC=20Ferro=20=EB=A1=9C=20=EC=93=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 셀프호스팅에 손대기 전의 강제 함수다. 아픈 자리를 전부 건드린다: R4 아래의 토큰 구조체, 태그드 유니온, 진단 출력, 유닛 경계. 토큰은 자기가 나온 글자를 담지 않는다. R4 가 대여를 집합 저장소에서 막으므로, 어디서 시작해 얼마나 긴지를 적고 소스는 옆에서 같이 다닌다. 위치도 &mut usize 로 옆에서 다닌다 -- 슬라이스와 함께 구조체에 들어갈 수 없기 때문이다. 이것이 R11 이 말하는 모양이고, 쓸 수 있다. first keyword unit @1 / number 42 @3 / text "hi" @3 / arrow -> @5 keyword 6 name 7 number 1 text 1 punct 15 / total 30 길에서 고친 것: - binding.Type.Variant 가 안 풀렸다. 유닛 경계 이름 조회가 심볼만 보고 타입을 보지 않았다. - 문자열 const 전역이 빈 슬라이스로 나갔다. 포인터는 링커만 아는 수라서 바이트에 구멍을 두고 링커가 채우게 한다. - exec.py 가 OUTPUT 마커를 여러 개 적어도 마지막 하나만 검사했다. 고치자마자 readfile 의 낡은 기대가 드러났다. run.py 217/217, exec.py 27/27. --- fec/src/checkcal.c | 18 +++++- fec/src/ir.c | 14 ++++ fec/src/ir.h | 13 ++++ fec/src/lowerstm.c | 35 ++++++++++ fec/src/x86.c | 27 ++++++-- fec/tests/exec/lexer/main.fe | 61 ++++++++++++++++++ fec/tests/exec/lexer/scan.fe | 122 +++++++++++++++++++++++++++++++++++ fec/tests/exec/lexer/tok.fe | 39 +++++++++++ fec/tests/exec/readfile.fe | 2 +- tests/exec.py | 18 ++++-- 10 files changed, 337 insertions(+), 12 deletions(-) create mode 100644 fec/tests/exec/lexer/main.fe create mode 100644 fec/tests/exec/lexer/scan.fe create mode 100644 fec/tests/exec/lexer/tok.fe diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index 3dfb14b..f24f9e8 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -16,7 +16,23 @@ FeType *cross_unit_value(FeCheckerState *s, FeNode *n, int *handled) if (!home) return 0; *handled=1; sym=unit_member(s->c,home,n->b && n->b->text ? n->b->text : ""); - if (!sym) { err(s->c,n->loc,"unknown name"); return unknown(s->c); } + if (!sym) { + /* A name in another unit can be a type as well as a value -- + `binding.Enum.Variant` reaches one through the other. */ + FeType *there=unit_type(s->c,home,n->b && n->b->text ? n->b->text : ""); + FeNode *decl=unit_type_decl(s->c,home, + n->b && n->b->text ? n->b->text : ""); + if (there && decl) { + if (!decl_is_public(decl)) { + err(s->c,n->loc,"type is private to its unit"); + return unknown(s->c); + } + n->sem_type=there; + return there; + } + err(s->c,n->loc,"unknown name"); + return unknown(s->c); + } if (!decl_is_public(sym->decl)) { err(s->c,n->loc,"name is private to its unit"); return unknown(s->c); diff --git a/fec/src/ir.c b/fec/src/ir.c index 0bb795c..6b5f117 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -106,6 +106,20 @@ FeIrGlobal *fe_ir_global(FeIrModule *m, const char *name, FeIrType type, return g; } +void fe_ir_global_ref(FeIrModule *m, FeIrGlobal *g, unsigned long at, + const char *symbol) +{ + FeIrReloc *grown; + if (!g) return; + grown = (FeIrReloc *)ir_alloc(m, (g->reloc_count + 1) * sizeof(FeIrReloc)); + if (!grown) return; + if (g->relocs) memcpy(grown, g->relocs, g->reloc_count * sizeof(FeIrReloc)); + grown[g->reloc_count].at = at; + grown[g->reloc_count].symbol = symbol; + g->relocs = grown; + ++g->reloc_count; +} + const char *fe_ir_string(FeIrModule *m, const char *bytes, unsigned long length) { FeIrGlobal *g; diff --git a/fec/src/ir.h b/fec/src/ir.h index 4db1269..beff64f 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -121,12 +121,22 @@ struct FeIrFunc { struct FeIrFunc *next; }; +/* A place inside a global's bytes that holds the address of something else. + The value is not known until the linker places it, so the bytes carry a hole + and this says what fills it. */ +typedef struct FeIrReloc { + unsigned long at; + const char *symbol; +} FeIrReloc; + typedef struct FeIrGlobal { const char *name; FeIrType type; unsigned long size; unsigned align; const unsigned char *init; /* size bytes, or null for zero */ + FeIrReloc *relocs; + unsigned reloc_count; struct FeIrGlobal *next; } FeIrGlobal; @@ -155,6 +165,9 @@ FeIrBlock *fe_ir_block(FeIrModule *m, FeIrFunc *f); FeIrGlobal *fe_ir_global(FeIrModule *m, const char *name, FeIrType type, unsigned long size, unsigned align, const unsigned char *init); +/* Say that `at` bytes into `g` there is the address of `symbol`. */ +void fe_ir_global_ref(FeIrModule *m, FeIrGlobal *g, unsigned long at, + const char *symbol); /* A string literal's bytes, interned so the same text is stored once. */ const char *fe_ir_string(FeIrModule *m, const char *bytes, unsigned long length); diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index a9dff93..df09f18 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -504,6 +504,41 @@ void lower_global(Lower *L, FeNode *n) unsigned char *init = 0; unsigned long size = ir_size(t); if (!n->cname) return; + /* A text constant is a pointer and a length. The pointer is not a number + anyone knows yet, so the bytes carry a hole and the linker fills it. */ + if (n->b && n->b->kind == FE_N_LITERAL && n->b->text && + n->b->text[0] == '"' && t && + (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR)) { + char text[1024]; + unsigned long raw = strlen(n->b->text); + unsigned long len = 0; + unsigned long i; + const char *label; + FeIrGlobal *g; + if (raw >= 2) raw -= 2; + for (i = 0; i < raw && len + 1 < sizeof text; ++i) { + char ch = n->b->text[1 + i]; + if (ch == 92 && i + 1 < raw) { + ++i; + switch (n->b->text[1 + i]) { + case 'n': ch = 10; break; + case 't': ch = 9; break; + case 'r': ch = 13; break; + case '0': ch = 0; break; + default: ch = n->b->text[1 + i]; break; + } + } + text[len++] = ch; + } + label = fe_ir_string(L->m, text, len); + init = (unsigned char *)fe_arena_alloc(&L->m->arena, 8); + if (!init || !label) return; + for (i = 0; i < 8; ++i) init[i] = 0; + for (i = 0; i < 4; ++i) init[4 + i] = (unsigned char)((len >> (i * 8)) & 0xFF); + g = fe_ir_global(L->m, n->cname, FE_IR_MEM, 8, 4, init); + fe_ir_global_ref(L->m, g, (unsigned long)SLICE_PTR_OFFSET, label); + return; + } if (n->b && n->b->kind == FE_N_LITERAL && size && size <= 8) { long v = literal_value(n->b); unsigned long i; diff --git a/fec/src/x86.c b/fec/src/x86.c index bec88fa..98e5824 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -634,10 +634,29 @@ void fe_x86_emit(const FeIrModule *m, FILE *out) fprintf(out, " db %lu dup(0)\n", g->size ? g->size : 1UL); continue; } - for (i = 0; i < g->size; ++i) { - if (i % 16 == 0) fputs(" db ", out); - fprintf(out, "%u%s", g->init[i], - (i + 1 == g->size || (i % 16) == 15) ? "\n" : ","); + for (i = 0; i < g->size; ) { + unsigned r; + unsigned long j; + for (r = 0; r < g->reloc_count; ++r) + if (g->relocs[r].at == i) break; + if (r < g->reloc_count) { + /* A hole the linker fills with an address. */ + fprintf(out, " dd offset %s\n", + g->relocs[r].symbol); + i += 4; + continue; + } + fputs(" db ", out); + j = 0; + while (i < g->size && j < 16) { + unsigned q; + for (q = 0; q < g->reloc_count; ++q) + if (g->relocs[q].at == i) break; + if (q < g->reloc_count) break; + fprintf(out, "%s%u", j ? "," : "", g->init[i]); + ++i; ++j; + } + fputc('\n', out); } if (!g->size) fputs(" db 0\n", out); } diff --git a/fec/tests/exec/lexer/main.fe b/fec/tests/exec/lexer/main.fe new file mode 100644 index 0000000..032dfa1 --- /dev/null +++ b/fec/tests/exec/lexer/main.fe @@ -0,0 +1,61 @@ +// EXIT:0 +// OUTPUT:first keyword unit @1 +// OUTPUT:number 42 @3 +// OUTPUT:text "hi" @3 +// OUTPUT:arrow -> @5 +// OUTPUT:keyword 6 name 7 number 1 text 1 punct 15 +// OUTPUT:total 30 +unit main; +import std.io; +import tok; +import scan; + +// The Ferro lexer, written in Ferro. This is the shape a self-hosted `fec` +// would take: read a source, hand back tokens, say where each came from. + +const SOURCE: str = "unit demo;\n\nfn answer() { let n = 42; let s = \"hi\"; }\n// a comment\nfn arrow() -> i32 { return n; }\n"; + +fn main() -> i32 { + var at: usize = 0; + var line: usize = 1; + var keywords: usize = 0; + var names: usize = 0; + var numbers: usize = 0; + var texts: usize = 0; + var puncts: usize = 0; + var total: usize = 0; + var first: bool = true; + while true { + let t: tok.Token = scan.next(SOURCE, &mut at, &mut line); + if t.kind == tok.Kind.End { break; } + total = total + 1; + if first { + @print("first {} {} @{}\n", tok.name_of(t.kind), + tok.text(SOURCE, t), t.line); + first = false; + } + match t.kind { + Keyword => { keywords = keywords + 1; } + Name => { names = names + 1; } + Number => { + numbers = numbers + 1; + @print("number {} @{}\n", tok.text(SOURCE, t), t.line); + } + Text => { + texts = texts + 1; + @print("text {} @{}\n", tok.text(SOURCE, t), t.line); + } + Punct => { + puncts = puncts + 1; + if t.len == 2 { + @print("arrow {} @{}\n", tok.text(SOURCE, t), t.line); + } + } + _ => { @print("unexpected {}\n", tok.name_of(t.kind)); } + } + } + @print("keyword {} name {} number {} text {} punct {}\n", + keywords, names, numbers, texts, puncts); + @print("total {}\n", total); + return 0; +} diff --git a/fec/tests/exec/lexer/scan.fe b/fec/tests/exec/lexer/scan.fe new file mode 100644 index 0000000..81e9ac5 --- /dev/null +++ b/fec/tests/exec/lexer/scan.fe @@ -0,0 +1,122 @@ +unit scan; +import tok; + +// A scanner over a byte slice. The position travels in a `&mut usize` beside +// the source rather than inside a struct with it, because a struct cannot hold +// a slice: a slice is a borrowed view and R4 keeps borrows out of aggregates. + +fn is_space(c: u8) -> bool { return c == 32 or c == 9 or c == 13 or c == 10; } +fn is_digit(c: u8) -> bool { return c >= 48 and c <= 57; } + +fn is_name_start(c: u8) -> bool { + if c >= 97 and c <= 122 { return true; } + if c >= 65 and c <= 90 { return true; } + return c == 95; +} + +fn is_name_part(c: u8) -> bool { + return is_name_start(c) or is_digit(c); +} + +const KEYWORDS: usize = 12; + +fn is_keyword(word: []u8) -> bool { + if same(word, "unit") { return true; } + if same(word, "import") { return true; } + if same(word, "pub") { return true; } + if same(word, "fn") { return true; } + if same(word, "struct") { return true; } + if same(word, "enum") { return true; } + if same(word, "let") { return true; } + if same(word, "var") { return true; } + if same(word, "if") { return true; } + if same(word, "else") { return true; } + if same(word, "while") { return true; } + if same(word, "return") { return true; } + return false; +} + +fn same(a: []u8, b: []u8) -> bool { + if a.n != b.n { return false; } + var i: usize = 0; + while i < a.n { + if a[i] != b[i] { return false; } + i = i + 1; + } + return true; +} + +/// Step over anything that is not a token: spaces, newlines, and `//` to the +/// end of the line. `line` counts what was crossed so a token can say where it +/// came from. +fn skip_gaps(src: []u8, at: &mut usize, line: &mut usize) -> void { + while at.^ < src.n { + let c: u8 = src[at.^]; + if c == 10 { line.^ = line.^ + 1; at.^ = at.^ + 1; } + else if is_space(c) { at.^ = at.^ + 1; } + else if c == 47 and at.^ + 1 < src.n and src[at.^ + 1] == 47 { + while at.^ < src.n { + if src[at.^] == 10 { break; } + at.^ = at.^ + 1; + } + } + else { break; } + } +} + +pub fn next(src: []u8, at: &mut usize, line: &mut usize) -> tok.Token { + skip_gaps(src, at, line); + let start: usize = at.^; + let where: usize = line.^; + if start >= src.n { + return tok.Token{ kind: tok.Kind.End, from: start, len: 0, line: where }; + } + let c: u8 = src[start]; + if is_name_start(c) { + while at.^ < src.n { + if not is_name_part(src[at.^]) { break; } + at.^ = at.^ + 1; + } + let word: []u8 = src[start..at.^]; + var kind: tok.Kind = tok.Kind.Name; + if is_keyword(word) { kind = tok.Kind.Keyword; } + return tok.Token{ kind: kind, from: start, len: at.^ - start, + line: where }; + } + if is_digit(c) { + while at.^ < src.n { + if not is_digit(src[at.^]) { break; } + at.^ = at.^ + 1; + } + return tok.Token{ kind: tok.Kind.Number, from: start, + len: at.^ - start, line: where }; + } + if c == 34 { + at.^ = at.^ + 1; + while at.^ < src.n { + if src[at.^] == 34 { break; } + if src[at.^] == 92 and at.^ + 1 < src.n { at.^ = at.^ + 1; } + at.^ = at.^ + 1; + } + if at.^ >= src.n { + return tok.Token{ kind: tok.Kind.Bad, from: start, + len: at.^ - start, line: where }; + } + at.^ = at.^ + 1; + return tok.Token{ kind: tok.Kind.Text, from: start, len: at.^ - start, + line: where }; + } + at.^ = at.^ + 1; + // Two-byte punctuation the language actually uses. + if at.^ < src.n { + let d: u8 = src[at.^]; + if c == 45 and d == 62 { at.^ = at.^ + 1; } + else if c == 61 and d == 61 { at.^ = at.^ + 1; } + else if c == 33 and d == 61 { at.^ = at.^ + 1; } + else if c == 60 and d == 61 { at.^ = at.^ + 1; } + else if c == 62 and d == 61 { at.^ = at.^ + 1; } + else if c == 46 and d == 46 { at.^ = at.^ + 1; } + } + return tok.Token{ kind: tok.Kind.Punct, from: start, len: at.^ - start, + line: where }; +} diff --git a/fec/tests/exec/lexer/tok.fe b/fec/tests/exec/lexer/tok.fe new file mode 100644 index 0000000..a5b9cb3 --- /dev/null +++ b/fec/tests/exec/lexer/tok.fe @@ -0,0 +1,39 @@ +unit tok; + +// What the lexer produces. A token does not hold the text it came from: R4 +// keeps borrows out of aggregate storage, so it records where in the source it +// starts and how long it is, and the source travels beside it. + +pub enum Kind { + End, + Name, + Number, + Text, + Punct, + Keyword, + Bad, +} + +pub struct Token { + pub kind: Kind, + pub from: usize, + pub len: usize, + pub line: usize, +} + +pub fn text(src: []u8, t: Token) -> []u8 { + return src[t.from..t.from + t.len]; +} + +pub fn name_of(k: Kind) -> []u8 { + match k { + End => { return "end"; } + Name => { return "name"; } + Number => { return "number"; } + Text => { return "text"; } + Punct => { return "punct"; } + Keyword => { return "keyword"; } + Bad => { return "bad"; } + } + return "?"; +} diff --git a/fec/tests/exec/readfile.fe b/fec/tests/exec/readfile.fe index a2888e0..f904eb8 100644 --- a/fec/tests/exec/readfile.fe +++ b/fec/tests/exec/readfile.fe @@ -1,5 +1,5 @@ // EXIT:0 -// OUTPUT:read 64 bytes +// OUTPUT:read 27 bytes // OUTPUT:first line: // EXIT:0 unit readfile; import std.io; diff --git a/tests/exec.py b/tests/exec.py index 92d0ea6..75ffd5f 100644 --- a/tests/exec.py +++ b/tests/exec.py @@ -35,14 +35,17 @@ def expectations(path: Path) -> dict: m = re.match(r"//\s*(EXIT|OUTPUT|NOCHECKS):(.*)", line) if m: key, value = m.group(1), m.group(2).strip() - want[key] = int(value) if key in ("EXIT", "NOCHECKS") else value + if key == "OUTPUT": + # Every OUTPUT line has to appear. Keeping only the last one + # would let the earlier ones rot unnoticed. + want.setdefault("OUTPUT", []).append(value) + else: + want[key] = int(value) return want def check_one(fec: Path, path: Path, out_dir: Path) -> tuple[bool, str]: want = expectations(path) - if "EXIT" not in want: - return False, "no // EXIT: marker" exe, log = builder.build(fec, path, out_dir) if not exe: @@ -52,8 +55,9 @@ def check_one(fec: Path, path: Path, out_dir: Path) -> tuple[bool, str]: code, text = builder.run(exe) if code != want["EXIT"]: return False, f"exited {code}, expected {want['EXIT']}\n {text.strip()}" - if "OUTPUT" in want and want["OUTPUT"] not in text: - return False, f"output has no {want['OUTPUT']!r}\n {text.strip()}" + for line in want.get("OUTPUT", []): + if line not in text: + return False, f"output has no {line!r}\n {text.strip()}" if "NOCHECKS" in want: exe2, log2 = builder.build(fec, path, out_dir / "nochecks", @@ -81,7 +85,9 @@ def main() -> int: if out_dir.exists(): shutil.rmtree(out_dir, ignore_errors=True) - cases = sorted(PROGRAMS.rglob("*.fe")) + # A file with no `// EXIT:` is a unit some program imports, not a program. + cases = [p for p in sorted(PROGRAMS.rglob("*.fe")) + if "EXIT" in expectations(p)] if args.select: cases = [p for p in cases if args.select in p.as_posix()] if not cases: From c7cba061b318662869d2ca3ac266501e1f2a776f Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 07:36:14 +0900 Subject: [PATCH 160/184] =?UTF-8?q?docs:=20=EC=85=80=ED=94=84=ED=98=B8?= =?UTF-8?q?=EC=8A=A4=ED=8C=85=20=EC=A0=84=20=EB=AA=A9=EB=A1=9D=EC=9D=B4=20?= =?UTF-8?q?=EB=81=9D=EB=82=9C=20=EC=83=81=ED=83=9C=EB=A5=BC=20=EC=A0=81?= =?UTF-8?q?=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 렉서가 알려준 것도 함께: R11 의 모양 -- 아레나가 소유하고 인덱스가 가리킨다 -- 은 쓸 수 있다. 토큰이 from/len 을 들고 소스가 옆에서 같이 다니는 것은 장황하지만 막히지 않는다. 모든 함수가 src 를 하나 더 받는 것이 그 값이다. 파일 크기 규칙을 AGENTS 에 넣었다. --- AGENTS.md | 6 ++++ TODO.md | 101 ++++++++++++++++++++++++++++++------------------------ 2 files changed, 63 insertions(+), 44 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fc524e3..103579c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,12 @@ uv run python tests/build.py <프로그램.fe> # 하나만 빌드해서 돌려 - 슬라이스 배치(포인터 다음 길이)와 wrapper 페이로드 위치는 각각 한 군데에만 적혀 있다. 두 군데가 되면 어긋난다. +## 파일 크기 + +**2,000 줄을 넘기지 않는다. 웬만하면 1,000 줄.** 넘어가면 나눈다. 나눌 때는 +줄 범위로 자르고 -- 주제별로 묶는 것보다 정확하다, 한 줄도 잃거나 겹치지 않으니 -- +공유하는 것은 비공개 헤더(`checkpri.h`, `lowerpri.h`)에 모은다. + ## 작업 흐름 - 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신한다. 구현이 명세와 다르면 둘 중 하나가 diff --git a/TODO.md b/TODO.md index 0026e7c..ccd4e50 100644 --- a/TODO.md +++ b/TODO.md @@ -1,50 +1,65 @@ # TODO ``` -uv run python tests/run.py 209/209 컴파일러가 프로그램에 대해 뭐라고 하는가 -uv run python tests/exec.py 21/21 컴파일된 프로그램이 실제로 무엇을 하는가 +uv run python tests/run.py 217/217 컴파일러가 프로그램에 대해 뭐라고 하는가 +uv run python tests/exec.py 27/27 컴파일된 프로그램이 실제로 무엇을 하는가 ``` -파이프라인이 끝에서 끝까지 돈다. - ``` .fe → fec → i386 asm → wasm → wlink → .exe → Windows 11 ``` --- -## 네 결정을 기다리는 것 - -세션 중에 **이동 규칙 두 곳을 완화**했다. 둘 다 R4(참조는 집합 저장소에 -못 들어감)가 아니라 이동 쪽이고, 되돌릴 수 있다. - -| | 무엇 | 왜 | 대안 | -|---|---|---|---| -| 1 | `&mut T`를 `&mut T` 파라미터에 넘기는 것은 이동이 아니라 **호출 동안의 재대여** | 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번뿐이라 `&mut`가 사실상 죽는다. SPEC §4.2에 이미 있던 "호출 인자 위치에서만" 재대여를 같은 종류끼리로 넓힌 것 | 되돌리면 재귀 하강 파서 같은 것을 못 쓴다 | -| 2 | 자기 `drop` 안에서는 필드를 꺼낼 수 있다 (R7 예외) | 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다. `mem.replace`로 우회하려면 유효한 대체값이 필요한데 그런 것이 없다 | `drop(self: Self)`로 값을 소비하게 바꾸면 R7을 안 건드려도 된다 | - ---- - -## 남은 일 - -| # | 일 | 규모 | 비고 | -|---|---|---|---| -| 1 | 레지스터 할당 | 중 | 지금은 임시값마다 스택 슬롯이다. IR이 "임시값은 블록을 넘지 않는다"라서 블록 단위 할당기면 충분하다 | -| 2 | `@print` 전개 | 소~중 | 지금은 `io.print` + `fmt.fmt_*`를 손으로 부른다. SPEC §6.3.1은 컴파일 단계 전개를 요구한다 | -| 3 | `std.map` | 중 | `std.list`는 있다. 해시는 아직 | -| 4 | `std.io` 읽기 | 소 | `Reader`, `io.read`. 지금은 쓰기만 | -| 5 | `match` 페이로드 | 중 | 태그 비교는 된다. 페이로드 바인딩은 아직 | -| 6 | `if let` | 소 | 프론트엔드는 검사한다. lowering이 아직 | -| 7 | 셀프호스팅 | 대 | `fec`을 Ferro로. 여기까지 오면 언어가 자기 무게를 견딘다는 증거 | - -## 미뤄둔 것 +## 셀프호스팅 전에 하기로 했던 것 — 전부 끝남 | | | |---|---| -| 인터럽트·공유 상태 | `interrupt` `shared` `atomic` `critical` — 파싱만 되고 의미 없음. SPEC §11에서 v0.2 | -| 다른 32비트 타깃 | m68k / ARM / MIPS / RV32. IR 결정은 전부 ISA 중립이라 백엔드만 붙이면 된다 | -| 엔디안 | 리틀엔디안 가정. `packed struct`가 바이트 배치를 약속하므로 빅엔디안 타깃이 생기면 타깃 파라미터가 된다 | -| fixture 이름 122개 | DOS 8.3 시절 잔재. 마커가 다 붙어서 `bad`/`ok` 접두사는 더 이상 기대값이 아니다 | +| `@print` / `@fprint` 전개 | SPEC §6.3.1 대로 컴파일 단계에서 편다. 진단 한 줄이 한 줄이다 | +| 파일과 명령줄 | 열기·읽기·쓰기·닫기, `argv`. 프로그램이 소스에 박힌 데이터 밖으로 나왔다 | +| `match` 페이로드와 `if let` | 태그드 유니온을 안전하게 해체한다 | +| 조용히 잘리던 상한 | 자란다. 넘쳐도 틀린 코드가 아니라 오류다 | +| `mem.Arena` | R11 이 말하는 아레나 + 핸들이 실제로 쓸 수 있다 | +| 레지스터 할당 | 블록 단위 선형 스캔. calc 4437 → 3796 줄 (-14%) | +| **Ferro 렉서를 Ferro 로** | 강제 함수. 돌아간다 | + +렉서가 알려준 것: **R11 의 모양(아레나가 소유하고 인덱스가 가리킨다)은 쓸 수 +있다.** 토큰이 `from`/`len` 을 들고 소스가 옆에서 같이 다니는 것은 장황하지만 +막히지 않는다. 모든 함수가 `src` 를 하나 더 받는 것이 값이다. + +--- + +## 셀프호스팅으로 가는 길 + +| # | 일 | 규모 | 비고 | +|---|---|---|---| +| 1 | Ferro 파서를 Ferro 로 | 대 | 렉서 다음. AST 를 아레나 + 핸들로 짓는다 | +| 2 | `std.map` | 중 | 심볼 표에 필요. 지금은 `List` 선형 탐색뿐 | +| 3 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | +| 4 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | +| 5 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | + +## 언어에 남은 구멍 + +| | | +|---|---| +| `[value; count]` 배열 반복 리터럴 | 없다. 큰 버퍼는 `undefined` 로 선언한다 | +| 정수 폭 섞임 | `1 + 함수호출()` 같은 데서 뻑뻑하다. 리터럴이 늘 맞춰주지 않는다 | +| `@sprint` | 전개하지 않는다. `@print`/`@fprint` 만 | +| `interrupt` `shared` `atomic` `critical` | 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | +| lowering 미구현 진단 | `internal: cannot lower X`. 사용자 오류처럼 보이지 않는다 | + +--- + +## 네 판단을 기다리는 것 + +세션 중에 **이동 규칙 두 곳을 완화**했다. R4(참조는 집합 저장소에 못 들어감)는 +건드리지 않았다. + +| | 무엇 | 왜 | 대안 | +|---|---|---|---| +| 1 | `&mut T` 를 `&mut T` 파라미터에 넘기는 것은 이동이 아니라 **호출 동안의 재대여** | 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번뿐이다. 렉서도 계산기도 이것 없이는 못 쓴다 | 되돌리면 재귀 하강 파서를 못 쓴다 | +| 2 | 자기 `drop` 안에서는 필드를 꺼낼 수 있다 (R7 예외) | 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다 | `drop(self: Self)` 로 값을 소비하게 바꾸면 R7 을 안 건드려도 된다 | --- @@ -54,18 +69,16 @@ uv run python tests/exec.py 21/21 컴파일된 프로그램이 실제로 |---|---| | 타깃 | **i386 하나.** 세그먼트 없음, `far` 영구 제외 (SPEC §2) | | `usize`/`isize` | **타깃의 포인터 폭.** 비트 수를 약속하지 않아 64비트 문이 닫히지 않음 | -| 제네릭 | 모노모피제이션. 순수 프론트엔드 기능이라 IR에 제네릭 개념이 없음 | -| 덩어리 전달 | 전부 주소로. 크기 임계값 없음 — ISA마다 다른 구조체 전달 ABI를 피해감 | -| 트랩 | `trap ` → `fe_trap(reason, UNIT_FILE, line)`. 유닛당 파일 문자열 하나 | -| 슬라이스 배치 | 포인터 다음 길이. 오프셋은 `lower.c` 한 군데에만 있다 | -| 오류 코드 | `error.Name`을 빌드 전체에서 모아 철자 순으로 1부터. 빌드 순서가 결과를 안 바꿈 | -| 셀프호스팅 | 640KB 목표 아님. 32비트 보호모드에서 돈다 (SPEC §2.1) | +| 제네릭 | 모노모피제이션. 순수 프론트엔드 기능이라 IR 에 제네릭 개념이 없음 | +| 덩어리 전달 | 전부 주소로. ISA 마다 다른 구조체 전달 ABI 를 피해감 | +| 트랩 | `trap ` → `fe_trap(reason, UNIT_FILE, line)` | +| 슬라이스 배치 | 포인터 다음 길이. 오프셋은 `lowerpri.h` 한 군데에만 | +| 오류 코드 | `error.Name` 을 빌드 전체에서 모아 철자 순으로 1부터 | +| 레지스터 | ebx·esi·edi 를 블록 안에 머무는 임시값에 준다. eax/ecx/edx 는 스크래치 | --- -## 위임 정책 +## 파일 크기 규칙 -**위임 가능** — 결과가 자명하고 판단이 없는 것. 파일 목록을 **명시적으로 열거**하고 -기준선 두 숫자를 주고 **변하면 안 된다**고 못박을 것. - -**위임 불가** — 언어 의미론 판단이 섞인 것. 마커 판정, IR/lowering/백엔드, stdlib 명세. +**2,000 줄을 넘기지 않는다. 웬만하면 1,000 줄.** 넘으면 나눈다 -- `check.c` 는 +3,937 줄이었고 `lower.c` 는 1,913 줄이었다. 지금 가장 큰 것은 1,000 줄이다. From 8c4e80e246f2a8b453449855a221e14cbc091a81 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 12:23:52 +0900 Subject: [PATCH 161/184] =?UTF-8?q?std:=20map=20=EC=9D=84=20=EC=93=B4?= =?UTF-8?q?=EB=8B=A4=20--=20=EB=B0=94=EC=9D=B4=ED=8A=B8=20=EC=97=B4?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B0=92=EC=9C=BC=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컴파일러는 이름을 끊임없이 찾는데 리스트 선형 탐색은 그 모양이 아니다. 키는 맵이 소유하는 한 버퍼에 복사되고 슬롯은 그 안의 어디인지만 적는다 -- R11 이 말하는 아레나와 핸들 모양이고, 그래서 맵을 놓는 것이 엔트리마다 하나가 아니라 두 번의 해제다. 개방 주소법에 선형 탐사. 표는 2의 거듭제곱이라 나눗셈이 아니라 마스크이고, 탐사가 길어지는 것이 표가 차는 것보다 먼저라 3/4 에서 자란다. 길에서 고친 것 넷: - 인스턴스를 만들 때 선언 유닛으로 전환하지 않아서, 필드 타입 Slot(V) 를 호출자 유닛에서 찾고 있었다. - mem.alloc_slice 가 원소 타입으로 단순한 이름만 받았다. Slot(V) 같은 인스턴스도 받는다. - 쓸 수 있는지를 바인딩이 아니라 소유된 것이 정한다. let p: ^[]mut T 는 p 를 고정하고 그것이 소유한 것은 쓸 수 있게 둔다. 슬라이스 인덱스도 마찬가지다. - 메서드 인자에 자유 함수와 같은 호출 한정 약화가 없었다. 알게 된 것: 대여는 루트 단위라 self 의 한 필드에 쓰는 동안 다른 필드를 읽을 수 없다. 지역으로 빼거나 메서드를 나누면 되지만, 필드 단위 대여가 있으면 훨씬 편할 자리다. count 5 / fn 2 let 3 missing 0 / grown 64 / after 40 / balanced 218/218, 28/28. --- fec/src/checkexp.c | 45 +++++++++-- fec/src/checkgen.c | 5 ++ fec/std/map.fe | 176 ++++++++++++++++++++++++++++++++++++++++- fec/tests/exec/maps.fe | 48 +++++++++++ 4 files changed, 265 insertions(+), 9 deletions(-) create mode 100644 fec/tests/exec/maps.fe diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index a8a9af0..a1b3bb5 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -142,9 +142,20 @@ int lvalue_writable(FeCheckerState *s, FeNode *n) t=n->a ? n->a->sem_type : 0; if (t && t->kind==FE_TYPE_REF && n->b && n->b->text && strcmp(n->b->text,"^")==0) return t->ref_mut; + /* Through an owner, what may be written is decided by what is owned, + not by whether the binding may be pointed somewhere else. `let p: + ^[]mut T` fixes p and leaves what it owns writable. */ + if (t && t->kind==FE_TYPE_OWNED && n->b && n->b->text && + strcmp(n->b->text,"^")==0) + return !t->elem || t->elem->kind!=FE_TYPE_SLICE || t->elem->ref_mut; + return lvalue_writable(s,n->a); + } + if (n->kind == FE_N_INDEX) { + /* An index into a slice asks the slice, not the binding. */ + t=n->a ? n->a->sem_type : 0; + if (t && t->kind==FE_TYPE_SLICE) return t->ref_mut; return lvalue_writable(s,n->a); } - if (n->kind == FE_N_INDEX) return lvalue_writable(s,n->a); return 0; } @@ -440,10 +451,16 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) if (strcmp(n->a->b->text,"alloc_slice")==0) { FeNode *count=arg ? arg->next : 0; FeType *item; - if(!arg || arg->kind!=FE_N_IDENT || !count || count->next) - err(c,n->loc,"mem.alloc_slice requires a type and length"); - item=arg && arg->kind==FE_N_IDENT ? - fe_type_intern(&c->types,arg->text) : unknown(c); + { + /* The element type may be an instance -- `Slot(V)` -- and + not just a name. */ + int named=0; + item=arg ? type_from_expr(s,arg,&named) : unknown(c); + if(!arg || !named || !count || count->next) { + err(c,n->loc,"mem.alloc_slice requires a type and length"); + item=unknown(c); + } + } b=count ? check_expr(s,count) : unknown(c); if(known(b) && !fe_type_is_integer(b)) err(c,count->loc,"slice length must be an integer"); @@ -578,9 +595,23 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) while(param && arg) { a=check_expr(s,arg); b=method_type(c,param->a,et); - if(!compatible(b,a,arg) && a->kind!=FE_TYPE_UNKNOWN) + /* A method argument gets the same call-only weakening a + free function's does: an exclusive view may be handed + over as a shared one for the length of the call, and an + exclusive borrow is lent rather than given. */ + if(!compatible(b,a,arg) && + !(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut && + fe_type_equal(b->elem,a->elem)) && + !(b && a && b->kind==FE_TYPE_REF && a->kind==FE_TYPE_REF && + !b->ref_mut && a->ref_mut && + fe_type_equal(b->elem,a->elem)) && + a->kind!=FE_TYPE_UNKNOWN) err(c,arg->loc,"method argument type mismatch"); - mark_moved(s,arg,a); + if(!call_reborrows(b,a) && + !(b && a && b->kind==FE_TYPE_SLICE && + a->kind==FE_TYPE_SLICE && !b->ref_mut && a->ref_mut)) + mark_moved(s,arg,a); param=param->next; arg=arg->next; } diff --git a/fec/src/checkgen.c b/fec/src/checkgen.c index 45c1a31..619213f 100644 --- a/fec/src/checkgen.c +++ b/fec/src/checkgen.c @@ -226,9 +226,13 @@ FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, for (f=decl->children;f;f=f->next) if (f->kind==FE_N_FIELD) ++fields; t->field_count=fields; if (fields) { + const char *save_unit=c->types.unit_name; t->fields=(FeFieldType *)fe_arena_alloc(&c->arena, fields*sizeof(FeFieldType)); if (!t->fields) { t->field_count=0; return t; } + /* Field types are written in the unit that declared the struct, not in + whichever unit asked for this instance. */ + c->types.unit_name=home->name; push_instance_bindings(c,&save,t); bind_self(c,t); i=0; @@ -240,6 +244,7 @@ FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, ++i; } pop_bindings(c,&save); + c->types.unit_name=save_unit; } fe_type_layout_all(&c->types); /* A type that says how to let go of itself needs that method to exist for diff --git a/fec/std/map.fe b/fec/std/map.fe index 5e238c6..3427f19 100644 --- a/fec/std/map.fe +++ b/fec/std/map.fe @@ -1,4 +1,176 @@ -unit map; -pub struct Map(K, V) { +unit std.map; + +// A table from a run of bytes to a value. +// +// A compiler looks names up constantly and a linear scan over a list is the +// wrong shape for that. Keys are copied into one buffer the map owns and each +// slot records where in it the key sits -- the arena-and-handle shape R11 asks +// for, which also means letting go of the map is two frees and not one per +// entry. +// +// Open addressing with linear probing. The table is a power of two so the +// index is a mask rather than a division, and it grows at three quarters full +// because probing gets long well before the table gets full. + +pub struct Slot(V) { + at: usize, len: usize, + used: bool, + value: V, +} + +pub struct Map(V) { + slots: ^[]mut Slot(V), + bytes: ^[]mut u8, + used_bytes: usize, + count: usize, + + pub fn with_capacity(n: usize) -> !Self { + var room: usize = 8; + while room < n * 2 { room = room * 2; } + let table: ^[]mut Slot(V) = try mem.alloc_slice(Slot(V), room); + let text: ^[]mut u8 = try mem.alloc_slice(u8, 64); + var i: usize = 0; + while i < room { + table.^[i].used = false; + i = i + 1; + } + return Self{ slots: table, bytes: text, used_bytes: 0, count: 0 }; + } + + pub fn count_of(self: &Self) -> usize { return self.count; } + + pub fn room(self: &Self) -> usize { return self.slots.^.n; } + + /// Where `key` sits in the table: the slot holding it, or the first free + /// slot it could go in. Probing stops at a free slot, which is why a slot + /// is never cleared -- only ever filled. + fn find(self: &Self, key: []u8) -> usize { + let mask: usize = self.slots.^.n - 1; + var at: usize = hash(key) & mask; + while true { + if not self.slots.^[at].used { return at; } + if self.same(at, key) { return at; } + at = (at + 1) & mask; + } + return 0; + } + + fn same(self: &Self, slot: usize, key: []u8) -> bool { + if self.slots.^[slot].len != key.n { return false; } + let from: usize = self.slots.^[slot].at; + var i: usize = 0; + while i < key.n { + if self.bytes.^[from + i] != key[i] { return false; } + i = i + 1; + } + return true; + } + + pub fn has(self: &Self, key: []u8) -> bool { + return self.slots.^[self.find(key)].used; + } + + pub fn get(self: &Self, key: []u8, missing: V) -> V { + let at: usize = self.find(key); + if self.slots.^[at].used { return self.slots.^[at].value; } + return missing; + } + + pub fn put(self: &mut Self, key: []u8, value: V) -> !void { + if self.count * 4 >= self.slots.^.n * 3 { try self.regrow(); } + let at: usize = self.find(key); + if self.slots.^[at].used { + self.slots.^[at].value = value; + return; + } + try self.keep(key, at); + self.slots.^[at].value = value; + self.slots.^[at].used = true; + self.count = self.count + 1; + return; + } + + /// Make sure `need` bytes fit, moving to a bigger buffer if they do not. + /// + /// Kept apart from `keep` because the borrow that swaps the buffer in is + /// a borrow of the whole of `self` -- borrowing is tracked at the root -- + /// and it has to be over before any field is read again. + fn ensure_room(self: &mut Self, need: usize) -> !void { + let have: usize = self.bytes.^.n; + if need <= have { return; } + var room: usize = have; + while room < need { room = room * 2; } + let bigger: ^[]mut u8 = try mem.alloc_slice(u8, room); + var k: usize = 0; + let filled: usize = self.used_bytes; + while k < filled { + bigger.^[k] = self.bytes.^[k]; + k = k + 1; + } + let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); + mem.destroy(old); + return; + } + + /// Copy `key` into the byte buffer and point the slot at it. + fn keep(self: &mut Self, key: []u8, slot: usize) -> !void { + let at: usize = self.used_bytes; + let need: usize = at + key.n; + try self.ensure_room(need); + var i: usize = 0; + while i < key.n { + self.bytes.^[at + i] = key[i]; + i = i + 1; + } + self.slots.^[slot].at = at; + self.slots.^[slot].len = key.n; + self.used_bytes = at + key.n; + return; + } + + /// Twice the slots, everything placed again. The keys do not move: they + /// live in the byte buffer and the slots only point at them. + fn regrow(self: &mut Self) -> !void { + let bigger: ^[]mut Slot(V) = try mem.alloc_slice(Slot(V), + self.slots.^.n * 2); + let mask: usize = bigger.^.n - 1; + var i: usize = 0; + while i < bigger.^.n { + bigger.^[i].used = false; + i = i + 1; + } + i = 0; + while i < self.slots.^.n { + if self.slots.^[i].used { + let from: usize = self.slots.^[i].at; + let len: usize = self.slots.^[i].len; + var at: usize = hash(self.bytes.^[from..from + len]) & mask; + while bigger.^[at].used { at = (at + 1) & mask; } + bigger.^[at] = self.slots.^[i]; + } + i = i + 1; + } + let old: ^[]mut Slot(V) = mem.replace(&mut self.slots, bigger); + mem.destroy(old); + return; + } + + pub fn drop(self: &mut Self) -> void { + mem.destroy(self.slots); + mem.destroy(self.bytes); + } +} + +/// FNV-1a. Small, fast, and good enough for identifiers; nothing here has to +/// resist an adversary choosing the keys. +pub fn hash(key: []u8) -> usize { + var h: u32 = 2166136261; + var i: usize = 0; + while i < key.n { + h = h ^ (key[i] as u32); + h = h * 16777619; + i = i + 1; + } + return h as usize; } diff --git a/fec/tests/exec/maps.fe b/fec/tests/exec/maps.fe new file mode 100644 index 0000000..c499f61 --- /dev/null +++ b/fec/tests/exec/maps.fe @@ -0,0 +1,48 @@ +// EXIT:0 +// OUTPUT:count 5 +// OUTPUT:fn 2 let 3 missing 0 +// OUTPUT:grown 64 +// OUTPUT:after 40 +// OUTPUT:balanced +unit maps; +import std.io; +import std.map; +import std.fmt; +import std.sys; + +fn run() -> !void { + var seen: map.Map(i32) = try map.Map(i32).with_capacity(4); + try seen.put("unit", 1); + try seen.put("fn", 2); + try seen.put("let", 3); + try seen.put("struct", 4); + try seen.put("return", 5); + @print("count {}\n", seen.count_of()); + @print("fn {} let {} missing {}\n", seen.get("fn", 0), seen.get("let", 0), + seen.get("nope", 0)); + // Enough keys to make it grow more than once. + var buf: [8]u8 = undefined; + var i: usize = 0; + while i < 40 { + let n: usize = fmt.fmt_i32(buf[..], i as i32); + try seen.put(buf[0..n], (i as i32) + 100); + i = i + 1; + } + @print("grown {}\n", seen.room()); + var found: usize = 0; + i = 0; + while i < 40 { + let n: usize = fmt.fmt_i32(buf[..], i as i32); + if seen.get(buf[0..n], 0) == (i as i32) + 100 { found = found + 1; } + i = i + 1; + } + @print("after {}\n", found); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() != sys.frees() { @print("leaked\n"); return 2; } + @print("balanced\n"); + return 0; +} From e84f89214762dd72a7dd14718465163657157696 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 12:49:53 +0900 Subject: [PATCH 162/184] =?UTF-8?q?Ferro=20=ED=8C=8C=EC=84=9C=EB=A5=BC=20F?= =?UTF-8?q?erro=20=EB=A1=9C,=20=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EA=B7=B8?= =?UTF-8?q?=EA=B2=83=EC=9D=B4=20=EB=93=9C=EB=9F=AC=EB=82=B8=20=EB=84=A4=20?= =?UTF-8?q?=EA=B0=80=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 렉서 다음은 파서다. 노드는 한 배열에 살고 자식은 그 안의 인덱스다 -- 노드는 ^Node 를 들 수 없고(여럿이며 한 번씩 소유하지 않는다) &Node 도 들 수 없다(R4). 인덱스는 둘 다 아니다. 소스도 필드가 아니라 매 단계에 같이 다닌다. unit demo / fn answer @2 / let n = (+ 1 (* 2 3)) / return n / balanced 전위 표기로 다시 찍는 것이 시험의 요점이다. 1 + 2 * 3 이 어떻게 묶였는지는 그렇게만 보인다. 쓰면서 나온 컴파일러 버그 넷: 1. 다른 유닛의 타입을 필드로 쓰면 그 필드 타입이 영영 UNKNOWN 이었다. 필드 해석이 유닛마다 선언 직후에 돌아서, 아직 선언되지 않은 유닛의 타입을 찾다 실패하고 그 답을 굳혔다. 이제 모든 유닛이 선언을 마친 뒤에 한 번 푼다. 2. 그리고 그 해석은 타입을 선언한 유닛에서 해야 한다. 필드 타입은 그 유닛의 import 로 쓰였는데 아무 유닛에서나 풀고 있었다. 타입 계층에 enter/leave 콜백을 두고 체커가 그 자리로 데려간다. 3. cycle_state 를 재귀 검사와 크기 계산이 같이 썼다. 첫 번째가 보는 중인 구조체 가 두 번째에게는 다 끝난 것으로 보여서, 필드가 하나뿐인 것처럼 1 바이트로 자리를 잡았다 -- Parser 가 그래서 자기 토큰을 밟았다. layout_state 로 나눴다. 4. 다른 유닛의 상수(ast.NONE)를 lowering 이 필드 접근으로 봤다. 체커가 이미 링크 이름을 붙여두었으니 그것이 있으면 전역이다. 그리고 R1 을 실제로 지키게 했다: 소유자를 놓으면 그것이 가진 것도 놓는다. 전에는 자기 drop 이 있거나 자기가 owned 일 때만이어서, drop 을 가진 타입을 필드로 담은 구조체는 그것을 놓을 방법이 없었다(drop 은 손으로 못 부른다). 이제 release_at 이 drop 을 부르고 필드로 내려간다. 그 덕에 List/Arena/Map 의 drop 이 전부 필요 없어져서 지웠다 -- 버퍼가 owned 이니 R1 이 알아서 한다. 221/221, 29/29. --- fec/src/check.c | 35 +++++ fec/src/checkpri.h | 1 + fec/src/checkpro.c | 4 +- fec/src/checkstm.c | 15 +- fec/src/lower.c | 11 +- fec/src/lowerexp.c | 58 +++++--- fec/src/lowerpri.h | 1 + fec/src/types.c | 70 +++++++--- fec/src/types.h | 11 ++ fec/std/list.fe | 10 +- fec/std/map.fe | 8 +- fec/std/mem.fe | 4 +- fec/tests/exec/lexer/ast.fe | 49 +++++++ fec/tests/exec/lexer/parse.fe | 256 ++++++++++++++++++++++++++++++++++ fec/tests/exec/lexer/tree.fe | 107 ++++++++++++++ 15 files changed, 582 insertions(+), 58 deletions(-) create mode 100644 fec/tests/exec/lexer/ast.fe create mode 100644 fec/tests/exec/lexer/parse.fe create mode 100644 fec/tests/exec/lexer/tree.fe diff --git a/fec/src/check.c b/fec/src/check.c index ea1a250..57ed1cf 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -284,6 +284,9 @@ void enter_unit(FeCheck *c, unsigned index) +static int enter_decl_hook(void *owner, const char *unit); +static void leave_decl_hook(void *owner, int back); + void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, unsigned pointer_bits, int no_checks) { @@ -301,6 +304,8 @@ void fe_check_init(FeCheck *c, FeBuild *build, FeDiags *diags, c->types.unit_name = "unit"; c->types.instantiate = instantiate_type_node; c->types.instantiate_owner = c; + c->types.enter_decl = enter_decl_hook; + c->types.leave_decl = leave_decl_hook; c->instances = (FeInstance *)fe_arena_alloc(&c->arena, (unsigned long)FE_GENERIC_INSTANCE_MAX * sizeof(FeInstance)); c->instance_count = 0; @@ -354,6 +359,36 @@ FeType *unit_type(FeCheck *c, FeUnit *u, const char *name) return 0; } +/* A field type is written in the unit that declared the type, so it has to be + resolved with that unit's imports in scope -- not with whichever unit + happens to be current when the walk reaches it. Returns the index to go back + to, or -1 when there is nowhere to go. */ +int enter_declaring_unit(FeCheck *c, const char *unit_name) +{ + unsigned i; + unsigned here; + if (!unit_name || !c->build || !c->unit) return -1; + here = unit_index(c,c->unit); + for (i=0;ibuild->count;++i) + if (strcmp(c->build->units[i].name,unit_name)==0) { + if (i==here) return -1; + enter_unit(c,i); + return (int)here; + } + return -1; +} + +/* The type layer calls these; it knows nothing about units beyond a name. */ +static int enter_decl_hook(void *owner, const char *unit) +{ + return enter_declaring_unit((FeCheck *)owner, unit); +} + +static void leave_decl_hook(void *owner, int back) +{ + enter_unit((FeCheck *)owner, (unsigned)back); +} + /* The AST declaration of a type another unit declares, for its visibility and for its methods. */ FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name) diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h index d3c04b7..fa75c95 100644 --- a/fec/src/checkpri.h +++ b/fec/src/checkpri.h @@ -104,6 +104,7 @@ FeUnit *binding_unit(FeCheckerState *s, FeNode *base); int decl_is_public(const FeNode *decl); FeSym *unit_member(FeCheck *c, FeUnit *u, const char *name); FeType *unit_type(FeCheck *c, FeUnit *u, const char *name); +int enter_declaring_unit(FeCheck *c, const char *unit_name); FeNode *unit_type_decl(FeCheck *c, FeUnit *u, const char *name); FeType *node_type_in(FeCheck *c, const char *unit, FeNode *node); FeNode *find_method(FeCheck *c, FeType *owner, const char *name); diff --git a/fec/src/checkpro.c b/fec/src/checkpro.c index 74acf81..0d67cd5 100644 --- a/fec/src/checkpro.c +++ b/fec/src/checkpro.c @@ -79,7 +79,6 @@ void declare_unit(FeCheck *c) fe_type_declare_enum(&c->types,n); for (n=c->ast->root ? c->ast->root->children : 0;n;n=n->next) if (n->kind==FE_N_ERROR_DECL) fe_type_declare_error(&c->types,n); - check_type_cycles(c); } /* The unit's top-level names, in a scope of their own so that another unit @@ -166,6 +165,9 @@ int fe_check_program(FeCheck *c) s.fn_node=0; fe_own_liveness_init(&s.liveness,&c->arena); for (u=0;ubuild->count;++u) { enter_unit(c,u); declare_unit(c); } + /* Only now: a field may name a type in a unit that had not declared it + yet, and resolving it early would freeze the wrong answer in place. */ + for (u=0;ubuild->count;++u) { enter_unit(c,u); check_type_cycles(c); } fe_type_layout_all(&c->types); for (u=0;ubuild->count;++u) { enter_unit(c,u); diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 458bd1c..4d28c1e 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -140,18 +140,25 @@ void check_type_cycle(FeCheck *c, FeType *t) if (t->kind == FE_TYPE_ARRAY) { check_type_cycle(c,t->elem); } else if (t->kind == FE_TYPE_STRUCT) { - for (i=0;ifield_count;i++) { + int back=enter_declaring_unit(c,t->unit); + for (i=0;ifield_count;i++) if (!t->fields[i].type && t->fields[i].ast_node) t->fields[i].type=fe_type_from_ast(&c->types,t->fields[i].ast_node->a); - check_type_cycle(c,t->fields[i].type); - } + if (back>=0) enter_unit(c,(unsigned)back); + for (i=0;ifield_count;i++) check_type_cycle(c,t->fields[i].type); } else if (t->kind == FE_TYPE_ENUM) { + int back=enter_declaring_unit(c,t->unit); for (i=0;ivariant_count;i++) { unsigned j; - for (j=0;jvariants[i].field_count;j++) { + for (j=0;jvariants[i].field_count;j++) if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) t->variants[i].fields[j].type=fe_type_from_ast(&c->types, t->variants[i].fields[j].ast_node->a); + } + if (back>=0) enter_unit(c,(unsigned)back); + for (i=0;ivariant_count;i++) { + unsigned j; + for (j=0;jvariants[i].field_count;j++) { next=t->variants[i].fields[j].type; check_type_cycle(c,next); } diff --git a/fec/src/lower.c b/fec/src/lower.c index 80d13f6..41ff46e 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -113,9 +113,18 @@ unsigned as_address(Lower *L, Slot s, FeNode *n) /* Does letting go of this type have to do something? */ int needs_release(const FeType *t) { + unsigned i; if (!t) return 0; if (t->kind == FE_TYPE_OWNED) return 1; - return t->has_drop != 0; + if (t->has_drop) return 1; + /* SPEC 5 R1: letting go of an owner lets go of what it owns. A struct that + holds an owner has something to do even when it says nothing itself -- + which is what lets one type hold another that has a `drop`, since + calling `drop` by hand is not allowed. */ + if (t->kind == FE_TYPE_STRUCT) + for (i = 0; i < t->field_count; ++i) + if (needs_release(t->fields[i].type)) return 1; + return 0; } int lower_reserve(Lower *L, void **items, unsigned *capacity, unsigned needed, diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c index 869a36f..40de08c 100644 --- a/fec/src/lowerexp.c +++ b/fec/src/lowerexp.c @@ -171,6 +171,12 @@ Slot lower_expr_core(Lower *L, FeNode *n) if (base && (base->kind == FE_TYPE_REF || base->kind == FE_TYPE_OWNED)) base = base->elem; field = fe_type_field(base, n->b && n->b->text ? n->b->text : ""); + /* `binding.name` is not a field of anything: it is a constant or a + global in another unit, and the checker already turned it into a + link name. */ + if (!field && n->cname) + return slot_place(fe_ir_at_global(n->cname, 0), it, + ir_size(t)); if (!field) { fail(L, "an unresolved field", n); return slot_void(); } b = lower_expr(L, n->a); if (n->a->sem_type && (n->a->sem_type->kind == FE_TYPE_REF || @@ -305,6 +311,37 @@ const char *drop_name(Lower *L, const FeType *t) return method->cname; } +/* Let go of one value sitting at `at`. A type that says how to let go of + itself is asked first; then whatever it holds is let go of in turn, so a + struct that owns a struct that owns a buffer settles all three without + anyone writing a `drop` (SPEC 5 R1). */ +void release_at(Lower *L, const FeType *t, FeIrPlace at) +{ + unsigned args[1]; + unsigned i; + if (!t) return; + if (t->has_drop) { + const char *how = drop_name(L, t); + args[0] = fe_ir_addr(L->m, L->b, at); + if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1); + } + if (t->kind == FE_TYPE_OWNED) { + FeIrPlace p = at; + if (t->elem && t->elem->kind == FE_TYPE_SLICE) + p.offset += SLICE_PTR_OFFSET; + args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, p); + fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); + return; + } + if (t->kind == FE_TYPE_STRUCT) + for (i = 0; i < t->field_count; ++i) { + FeIrPlace p = at; + if (!needs_release(t->fields[i].type)) continue; + p.offset += (long)t->fields[i].offset; + release_at(L, t->fields[i].type, p); + } +} + /* Settle what a scope owes, most recent first. A `return` in the middle of a function still owes everything, so every exit path calls this. */ void run_deferred(Lower *L, unsigned from) @@ -321,29 +358,10 @@ void run_deferred(Lower *L, unsigned from) fe_ir_at_local(L->owed[i - 1].flag, 0)); FeIrBlock *doit = new_block(L); FeIrBlock *skip = new_block(L); - unsigned args[1]; FeType *t = L->owed[i - 1].type; fe_ir_br(L->b, live, doit->id, skip->id); L->b = doit; - if (t && t->kind == FE_TYPE_OWNED && t->elem && - t->elem->kind == FE_TYPE_SLICE) { - FeIrPlace at = fe_ir_at_local(L->owed[i - 1].local, - SLICE_PTR_OFFSET); - args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, at); - } else { - args[0] = fe_ir_load(L->m, L->b, FE_IR_PTR, - fe_ir_at_local(L->owed[i - 1].local, 0)); - } - if (t && t->has_drop) { - /* A type that says how to let go of itself is asked to; the - name is the one its instance was given. */ - const char *how = drop_name(L, t); - args[0] = fe_ir_addr(L->m, L->b, - fe_ir_at_local(L->owed[i - 1].local, 0)); - if (how) fe_ir_call(L->m, L->b, FE_IR_VOID, how, args, 1); - } else { - fe_ir_call(L->m, L->b, FE_IR_VOID, "fe_rt_free", args, 1); - } + release_at(L, t, fe_ir_at_local(L->owed[i - 1].local, 0)); fe_ir_jmp(L->b, skip->id); L->b = skip; } diff --git a/fec/src/lowerpri.h b/fec/src/lowerpri.h index 3c5522d..9ebd779 100644 --- a/fec/src/lowerpri.h +++ b/fec/src/lowerpri.h @@ -145,6 +145,7 @@ Slot lower_call(Lower *L, FeNode *n); Slot lower_expr(Lower *L, FeNode *n); Slot lower_expr_core(Lower *L, FeNode *n); const char *drop_name(Lower *L, const FeType *t); +void release_at(Lower *L, const FeType *t, FeIrPlace at); void run_deferred(Lower *L, unsigned from); Slot wrap_context(Lower *L, Slot v, FeNode *n); unsigned wrapper_tag(Lower *L, Slot w, const FeType *t, FeNode *n); diff --git a/fec/src/types.c b/fec/src/types.c index 1c1b707..b7b7cff 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -47,6 +47,7 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->next = ctx->types; t->emit_state = 0; t->cycle_state = 0; + t->layout_state = 0; ctx->types = t; return t; } @@ -61,6 +62,8 @@ void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits) ctx->param_count = 0; ctx->instantiate = 0; ctx->instantiate_owner = 0; + ctx->enter_decl = 0; + ctx->leave_decl = 0; } /* Does this type answer to `name` for someone checking `unit`? A type with no @@ -452,6 +455,26 @@ unsigned fe_type_align(const FeType *t) return t && t->align ? t->align : 1U; } +/* Resolve this type's fields where they were written. Without the callback + installed -- or for a type nobody declared -- everything stays where it is, + which is what the non-checking users of this layer want. */ +static int enter_decl_unit(FeTypeCtx *ctx, const char *unit, const char **was) +{ + *was = ctx->unit_name; + if (!ctx->enter_decl || !unit) return -1; + return ctx->enter_decl(ctx->instantiate_owner, unit); +} + +/* Put back both halves: the unit the checker was in, and the name this layer + was interning under -- an instantiation moves the second without the + first, so restoring one is not restoring the other. */ +static void leave_decl_unit(FeTypeCtx *ctx, int back, const char *was) +{ + if (back >= 0 && ctx->leave_decl) + ctx->leave_decl(ctx->instantiate_owner, back); + ctx->unit_name = was; +} + static void layout_type(FeTypeCtx *ctx, FeType *t) { unsigned i; @@ -460,14 +483,14 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) unsigned long max_size; unsigned max_align; if (!t || t->size) return; - if (t->cycle_state == 1) { + if (t->layout_state == 1) { t->size = 1; t->align = 1; return; } - t->cycle_state = 1; + t->layout_state = 1; if (t->kind == FE_TYPE_VOID || t->kind == FE_TYPE_UNKNOWN || - t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->cycle_state = 2; return; } + t->kind == FE_TYPE_ERROR) { t->size = 0; t->align = 1; t->layout_state = 2; return; } if (t->kind == FE_TYPE_ERROR_UNION) { if (t->error_value && t->error_value->kind != FE_TYPE_VOID) { layout_type(ctx,t->error_value); @@ -478,7 +501,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) t->size=2; t->align=2U; } - t->cycle_state = 2; return; + t->layout_state = 2; return; } if (t->kind == FE_TYPE_OPTIONAL) { layout_type(ctx,t->elem); @@ -490,44 +513,48 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) t->size=round_up(1UL,t->align)+fe_type_size(t->elem); t->size=round_up(t->size,t->align); } - t->cycle_state=2; return; + t->layout_state=2; return; } if (t->kind == FE_TYPE_BOOL || t->kind == FE_TYPE_CHAR) { - t->size = 1; t->align = 1; t->cycle_state = 2; return; + t->size = 1; t->align = 1; t->layout_state = 2; return; } if (t->kind == FE_TYPE_INT) { t->size = (t->bits + 7U) / 8U; t->align = t->size; if (t->size > 4UL) t->size = 4UL; - t->cycle_state = 2; return; + t->layout_state = 2; return; } if (t->kind == FE_TYPE_REF || t->kind == FE_TYPE_RAW) { t->size = FE_PTR_SIZE; t->align = FE_PTR_ALIGN; - t->cycle_state = 2; return; + t->layout_state = 2; return; } if (t->kind == FE_TYPE_OWNED) { t->size = t->elem && t->elem->kind==FE_TYPE_SLICE ? 2UL * FE_PTR_SIZE : FE_PTR_SIZE; t->align = FE_PTR_ALIGN; - t->cycle_state = 2; return; + t->layout_state = 2; return; } if (t->kind == FE_TYPE_SLICE || t->kind == FE_TYPE_STR) { t->size = 2UL * FE_PTR_SIZE; t->align = FE_PTR_ALIGN; - t->cycle_state = 2; return; + t->layout_state = 2; return; } if (t->kind == FE_TYPE_ARRAY) { layout_type(ctx, t->elem); t->align = t->packed ? 1U : fe_type_align(t->elem); t->size = t->length * fe_type_size(t->elem); - t->cycle_state = 2; return; + t->layout_state = 2; return; } if (t->kind == FE_TYPE_STRUCT) { - off = 0; max_align = 1; - for (i = 0; i < t->field_count; ++i) { + const char *was; + int back = enter_decl_unit(ctx, t->unit, &was); + for (i = 0; i < t->field_count; ++i) if (!t->fields[i].type && t->fields[i].ast_node) t->fields[i].type = fe_type_from_ast(ctx, t->fields[i].ast_node->a); + leave_decl_unit(ctx, back, was); + off = 0; max_align = 1; + for (i = 0; i < t->field_count; ++i) { layout_type(ctx, t->fields[i].type); align = t->packed ? 1U : fe_type_align(t->fields[i].type); if (align > max_align) max_align = align; @@ -537,18 +564,25 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) } t->align = max_align; t->size = round_up(off, max_align); - t->cycle_state = 2; + t->layout_state = 2; return; } if (t->kind == FE_TYPE_ENUM) { + const char *was; + int back = enter_decl_unit(ctx, t->unit, &was); + for (i = 0; i < t->variant_count; ++i) { + unsigned j; + for (j = 0; j < t->variants[i].field_count; ++j) + if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) + t->variants[i].fields[j].type = fe_type_from_ast( + ctx, t->variants[i].fields[j].ast_node->a); + } + leave_decl_unit(ctx, back, was); max_size = 0; max_align = 1; for (i = 0; i < t->variant_count; ++i) { unsigned j; off = 0; for (j = 0; j < t->variants[i].field_count; ++j) { - if (!t->variants[i].fields[j].type && t->variants[i].fields[j].ast_node) - t->variants[i].fields[j].type = fe_type_from_ast( - ctx, t->variants[i].fields[j].ast_node->a); layout_type(ctx, t->variants[i].fields[j].type); if (fe_type_align(t->variants[i].fields[j].type) > max_align) max_align = fe_type_align(t->variants[i].fields[j].type); @@ -564,7 +598,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) off = round_up(t->bits / 8U, max_align); t->size = round_up(off + max_size, max_align); t->align = max_align; - t->cycle_state = 2; + t->layout_state = 2; } } diff --git a/fec/src/types.h b/fec/src/types.h index 4109839..d8db1ea 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -98,6 +98,11 @@ struct FeType { FeType *next; int emit_state; int cycle_state; + /* Separate from `cycle_state`: the checker's by-value recursion walk and + this layer's size computation run inside one another, and sharing one + marker made a struct in the middle of the first look complete to the + second -- one byte wide, with every field on top of the next. */ + int layout_state; }; typedef struct FeTypeCtx { @@ -114,6 +119,12 @@ typedef struct FeTypeCtx { so it installs this and the type layer calls back into it. */ FeType *(*instantiate)(void *owner, const FeNode *node); void *instantiate_owner; + /* A field type is written in the unit that declared it, so resolving one + has to happen with that unit's imports in scope. The checker owns that + knowledge, so it installs this pair and the type layer calls back. + `enter` answers with what to hand `leave`, or -1 for "stayed put". */ + int (*enter_decl)(void *owner, const char *unit); + void (*leave_decl)(void *owner, int back); } FeTypeCtx; void fe_types_init(FeTypeCtx *ctx, FeArena *arena, unsigned pointer_bits); diff --git a/fec/std/list.fe b/fec/std/list.fe index 58fff67..cdf7c36 100644 --- a/fec/std/list.fe +++ b/fec/std/list.fe @@ -1,8 +1,10 @@ unit std.list; // A growable sequence. The buffer is owned, so a List owns its elements and -// releasing it releases them (SPEC 5 R1). Growth doubles, which keeps the -// total copying proportional to the number of pushes. +// releasing it releases them (SPEC 5 R1) -- which is also why there is no +// `drop` here: letting go of a List lets go of its buffer on its own. Growth +// doubles, which keeps the total copying proportional to the number of +// pushes. pub struct List(T) { items: ^[]mut T, @@ -46,8 +48,4 @@ pub struct List(T) { mem.destroy(old); return; } - - pub fn drop(self: &mut Self) -> void { - mem.destroy(self.items); - } } diff --git a/fec/std/map.fe b/fec/std/map.fe index 3427f19..3b50ec7 100644 --- a/fec/std/map.fe +++ b/fec/std/map.fe @@ -6,7 +6,8 @@ unit std.map; // wrong shape for that. Keys are copied into one buffer the map owns and each // slot records where in it the key sits -- the arena-and-handle shape R11 asks // for, which also means letting go of the map is two frees and not one per -// entry. +// entry. Both buffers are owned, so R1 does the letting go and no `drop` is +// written here. // // Open addressing with linear probing. The table is a power of two so the // index is a mask rather than a division, and it grows at three quarters full @@ -155,11 +156,6 @@ pub struct Map(V) { mem.destroy(old); return; } - - pub fn drop(self: &mut Self) -> void { - mem.destroy(self.slots); - mem.destroy(self.bytes); - } } /// FNV-1a. Small, fast, and good enough for identifiers; nothing here has to diff --git a/fec/std/mem.fe b/fec/std/mem.fe index f4524f7..831cccc 100644 --- a/fec/std/mem.fe +++ b/fec/std/mem.fe @@ -10,7 +10,8 @@ import std.sys; /// SPEC R11 answers recursive and graph-shaped data with an arena that owns /// the values and integer handles that reference them. This is that arena. A /// handle is an offset, so it stays valid while the arena does, and comparing -/// two handles is comparing two numbers. +/// two handles is comparing two numbers. The block itself is owned, so nothing +/// here says how to let go of it -- R1 already does. pub struct Arena { bytes: ^[]mut u8, used: usize, @@ -56,5 +57,4 @@ pub struct Arena { /// that is the trade an arena makes. pub fn reset(self: &mut Self) -> void { self.used = 0; } - pub fn drop(self: &mut Self) -> void { mem.destroy(self.bytes); } } diff --git a/fec/tests/exec/lexer/ast.fe b/fec/tests/exec/lexer/ast.fe new file mode 100644 index 0000000..9219e43 --- /dev/null +++ b/fec/tests/exec/lexer/ast.fe @@ -0,0 +1,49 @@ +unit ast; + +// The tree the parser builds. +// +// Nodes live in one growing array and refer to each other by index, which is +// what SPEC R11 asks for: an owner that holds the values and handles that +// point at them. A node cannot hold a `^Node` for its children because a node +// has several and they are not each owned once; it cannot hold a `&Node` +// because R4 keeps borrows out of aggregate storage. An index is neither. + +pub enum Shape { + Unit, // a: name + Fn, // a: name, b: first statement + Let, // a: name, b: value + Return, // a: value, or NONE + Binary, // a: left, b: right, from/len: the operator + Number, + Name, + Text, + Error, +} + +/// No node. Zero is a real index, so the empty handle is the largest one. +pub const NONE: usize = 4294967295; + +pub struct Node { + pub shape: Shape, + pub from: usize, // where in the source this came from + pub len: usize, + pub line: usize, + pub a: usize, // handles into the same tree + pub b: usize, + pub next: usize, // the following statement, when there is one +} + +pub fn name_of(s: Shape) -> []u8 { + match s { + Unit => { return "unit"; } + Fn => { return "fn"; } + Let => { return "let"; } + Return => { return "return"; } + Binary => { return "binary"; } + Number => { return "number"; } + Name => { return "name"; } + Text => { return "text"; } + Error => { return "error"; } + } + return "?"; +} diff --git a/fec/tests/exec/lexer/parse.fe b/fec/tests/exec/lexer/parse.fe new file mode 100644 index 0000000..ffd5d2b --- /dev/null +++ b/fec/tests/exec/lexer/parse.fe @@ -0,0 +1,256 @@ +unit parse; + +import std.list; +import std.str; +import tok; +import scan; +import ast; + +// Recursive descent over the lexer's tokens. +// +// The source is not a field: R4 keeps borrows out of aggregate storage, so +// `src` is passed to every step, the same way the lexer passes it. What the +// parser does own is the node array, and every parent points at its children +// by index into it. +// +// One token of lookahead lives in `cur`. That is all this grammar needs. + +pub struct Parser { + nodes: list.List(ast.Node), + at: usize, + line: usize, + cur: tok.Token, + pub errors: usize, + + pub fn on(src: []u8) -> !Self { + let nodes: list.List(ast.Node) = + try list.List(ast.Node).with_capacity(16); + var p: Self = Self{ + nodes: nodes, + at: 0, + line: 1, + cur: tok.Token{ kind: tok.Kind.End, from: 0, len: 0, line: 1 }, + errors: 0, + }; + p.bump(src); + return p; + } + + fn bump(self: &mut Self, src: []u8) -> void { + self.cur = scan.next(src, &mut self.at, &mut self.line); + return; + } + + fn done(self: &Self) -> bool { return self.cur.kind == tok.Kind.End; } + + /// Is the token in hand this exact spelling? + fn is(self: &Self, src: []u8, want: []u8) -> bool { + return str.eq(tok.text(src, self.cur), want); + } + + /// Take the token in hand if it is this spelling; say whether it was. + fn eat(self: &mut Self, src: []u8, want: []u8) -> bool { + if not self.is(src, want) { return false; } + self.bump(src); + return true; + } + + /// Demand this spelling. A miss is counted and the token stays put, so the + /// caller decides how to get back on its feet. + fn want(self: &mut Self, src: []u8, w: []u8) -> bool { + if self.eat(src, w) { return true; } + self.errors = self.errors + 1; + return false; + } + + fn add(self: &mut Self, s: ast.Shape, t: tok.Token, a: usize, + b: usize) -> !usize { + let n: ast.Node = ast.Node{ + shape: s, from: t.from, len: t.len, line: t.line, + a: a, b: b, next: ast.NONE, + }; + let i: usize = self.nodes.count(); + try self.nodes.push(n); + return i; + } + + // -- reading the tree back ------------------------------------------ + + pub fn count(self: &Self) -> usize { return self.nodes.count(); } + + pub fn node(self: &Self, i: usize) -> ast.Node { + return self.nodes.at(i); + } + + + // -- the grammar ---------------------------------------------------- + // + // unit := "unit" NAME ";" item* + // item := "fn" NAME "(" ")" block + // block := "{" stmt* "}" + // stmt := "let" NAME "=" expr ";" | "return" expr? ";" + // expr := term (("+" | "-") term)* + // term := factor (("*" | "/") factor)* + // factor := NUMBER | NAME | TEXT | "(" expr ")" + + pub fn unit_decl(self: &mut Self, src: []u8) -> !usize { + let ok: bool = self.want(src, "unit"); + let name: tok.Token = self.cur; + if ok { self.bump(src); } + let semi: bool = self.want(src, ";"); + let root: usize = try self.add(ast.Shape.Unit, name, ast.NONE, + ast.NONE); + var first: usize = ast.NONE; + var last: usize = ast.NONE; + while not self.done() { + let it: usize = try self.item(src); + if first == ast.NONE { first = it; } + else { self.link(last, it); } + last = it; + } + self.set_a(root, first); + return root; + } + + /// Point one statement or item at the one after it. + fn link(self: &mut Self, from: usize, to: usize) -> void { + var n: ast.Node = self.nodes.at(from); + n.next = to; + self.nodes.set(from, n); + return; + } + + fn set_a(self: &mut Self, at: usize, a: usize) -> void { + var n: ast.Node = self.nodes.at(at); + n.a = a; + self.nodes.set(at, n); + return; + } + + fn item(self: &mut Self, src: []u8) -> !usize { + if not self.is(src, "fn") { + let bad: tok.Token = self.cur; + self.errors = self.errors + 1; + self.skip_stmt(src); + return try self.add(ast.Shape.Error, bad, ast.NONE, ast.NONE); + } + self.bump(src); + let name: tok.Token = self.cur; + self.bump(src); + let open: bool = self.want(src, "("); + let close: bool = self.want(src, ")"); + let body: usize = try self.block(src); + return try self.add(ast.Shape.Fn, name, ast.NONE, body); + } + + fn block(self: &mut Self, src: []u8) -> !usize { + let open: bool = self.want(src, "{"); + var first: usize = ast.NONE; + var last: usize = ast.NONE; + while true { + if self.done() { break; } + if self.is(src, "}") { break; } + let s: usize = try self.stmt(src); + if first == ast.NONE { first = s; } + else { self.link(last, s); } + last = s; + } + let close: bool = self.want(src, "}"); + return first; + } + + fn stmt(self: &mut Self, src: []u8) -> !usize { + if self.is(src, "let") { + self.bump(src); + let name: tok.Token = self.cur; + self.bump(src); + let has_eq: bool = self.want(src, "="); + let value: usize = try self.expr(src); + let semi: bool = self.want(src, ";"); + return try self.add(ast.Shape.Let, name, ast.NONE, value); + } + if self.is(src, "return") { + let head: tok.Token = self.cur; + self.bump(src); + if self.is(src, ";") { + self.bump(src); + return try self.add(ast.Shape.Return, head, ast.NONE, + ast.NONE); + } + let value: usize = try self.expr(src); + let semi: bool = self.want(src, ";"); + return try self.add(ast.Shape.Return, head, value, ast.NONE); + } + let bad: tok.Token = self.cur; + self.errors = self.errors + 1; + self.skip_stmt(src); + return try self.add(ast.Shape.Error, bad, ast.NONE, ast.NONE); + } + + /// Get back to a statement boundary after something unrecognised. Stopping + /// at `;` or `}` means one bad statement costs one diagnostic, not a run + /// of them. + fn skip_stmt(self: &mut Self, src: []u8) -> void { + while not self.done() { + if self.is(src, "}") { return; } + if self.is(src, ";") { self.bump(src); return; } + self.bump(src); + } + return; + } + + fn expr(self: &mut Self, src: []u8) -> !usize { + var left: usize = try self.term(src); + while true { + if self.done() { break; } + let plus: bool = self.is(src, "+"); + let minus: bool = self.is(src, "-"); + if not plus and not minus { break; } + let op: tok.Token = self.cur; + self.bump(src); + let right: usize = try self.term(src); + left = try self.add(ast.Shape.Binary, op, left, right); + } + return left; + } + + fn term(self: &mut Self, src: []u8) -> !usize { + var left: usize = try self.factor(src); + while true { + if self.done() { break; } + let star: bool = self.is(src, "*"); + let slash: bool = self.is(src, "/"); + if not star and not slash { break; } + let op: tok.Token = self.cur; + self.bump(src); + let right: usize = try self.factor(src); + left = try self.add(ast.Shape.Binary, op, left, right); + } + return left; + } + + fn factor(self: &mut Self, src: []u8) -> !usize { + let t: tok.Token = self.cur; + if t.kind == tok.Kind.Number { + self.bump(src); + return try self.add(ast.Shape.Number, t, ast.NONE, ast.NONE); + } + if t.kind == tok.Kind.Name { + self.bump(src); + return try self.add(ast.Shape.Name, t, ast.NONE, ast.NONE); + } + if t.kind == tok.Kind.Text { + self.bump(src); + return try self.add(ast.Shape.Text, t, ast.NONE, ast.NONE); + } + if self.is(src, "(") { + self.bump(src); + let inner: usize = try self.expr(src); + let close: bool = self.want(src, ")"); + return inner; + } + self.errors = self.errors + 1; + self.bump(src); + return try self.add(ast.Shape.Error, t, ast.NONE, ast.NONE); + } +} diff --git a/fec/tests/exec/lexer/tree.fe b/fec/tests/exec/lexer/tree.fe new file mode 100644 index 0000000..4f7e2e6 --- /dev/null +++ b/fec/tests/exec/lexer/tree.fe @@ -0,0 +1,107 @@ +// EXIT:0 +// OUTPUT:unit demo +// OUTPUT:fn answer @2 +// OUTPUT: let n = (+ 1 (* 2 3)) +// OUTPUT: return n +// OUTPUT:fn greet @3 +// OUTPUT: let s = "hi" +// OUTPUT: return +// OUTPUT:fn muddle @4 +// OUTPUT: let x = +// OUTPUT:nodes 17 errors 2 +// OUTPUT:balanced +unit tree; + +import std.io; +import std.sys; +import tok; +import ast; +import parse; + +// The Ferro parser, written in Ferro. +// +// The lexer next to this file showed that a token can say where it came from +// instead of holding the text. A tree is the same idea one level up: a node +// cannot hold `^Node` children -- it has several and owns none of them once -- +// and R4 keeps `&Node` out of aggregate storage. So the parser owns one array +// of nodes and every child is an index into it. +// +// Printing the expressions back in prefix form is the point of the test: it is +// the only way to see that `1 + 2 * 3` bound the way the grammar says. + +const SOURCE: str = "unit demo;\nfn answer() { let n = 1 + 2 * 3; return n; }\nfn greet() { let s = \"hi\"; return; }\nfn muddle() { let x = ; }\n"; + +fn show_expr(p: &parse.Parser, src: []u8, i: usize) -> void { + if i == ast.NONE { return; } + let n: ast.Node = p.node(i); + match n.shape { + Binary => { + @print("({} ", src[n.from..n.from + n.len]); + show_expr(p, src, n.a); + @print(" "); + show_expr(p, src, n.b); + @print(")"); + } + Error => { @print(""); } + _ => { @print("{}", src[n.from..n.from + n.len]); } + } + return; +} + +fn show_stmt(p: &parse.Parser, src: []u8, i: usize) -> void { + let n: ast.Node = p.node(i); + match n.shape { + Let => { + @print(" let {} = ", src[n.from..n.from + n.len]); + show_expr(p, src, n.b); + @print("\n"); + } + Return => { + if n.a == ast.NONE { @print(" return\n"); } + else { + @print(" return "); + show_expr(p, src, n.a); + @print("\n"); + } + } + _ => { @print(" \n"); } + } + return; +} + +fn show_item(p: &parse.Parser, src: []u8, i: usize) -> void { + let n: ast.Node = p.node(i); + match n.shape { + Fn => { + @print("fn {} @{}\n", src[n.from..n.from + n.len], n.line); + var s: usize = n.b; + while s != ast.NONE { + show_stmt(p, src, s); + s = p.node(s).next; + } + } + _ => { @print(" \n"); } + } + return; +} + +fn run() -> !void { + var p: parse.Parser = try parse.Parser.on(SOURCE); + let root: usize = try p.unit_decl(SOURCE); + let head: ast.Node = p.node(root); + @print("unit {}\n", SOURCE[head.from..head.from + head.len]); + var it: usize = head.a; + while it != ast.NONE { + show_item(&p, SOURCE, it); + it = p.node(it).next; + } + @print("nodes {} errors {}\n", p.count(), p.errors); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("out of memory\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked {}\n", sys.allocs() - sys.frees()); } + return 0; +} From abdb049d05296b408b217b7c78abe84ed47945d4 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 12:52:38 +0900 Subject: [PATCH 163/184] =?UTF-8?q?lowering:=20.n=20=EC=9D=80=20=EC=8A=AC?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EC=8A=A4=EC=97=90=EA=B2=8C=EB=A7=8C=20?= =?UTF-8?q?=EA=B8=B8=EC=9D=B4=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구조체도 필드를 n 이라 부를 수 있다. lowering 은 이름만 보고 슬라이스 길이 자리(포인터 다음 4바이트)를 읽어서, 그 자리에 있던 그럴듯한 숫자를 돌려주고 있었다. 체커는 제대로 필드로 풀고 있었으니 같은 함수 안에서 쓰기와 읽기가 어긋났다. Box{ room: ^[]mut u8, n: usize, m: usize } n 777 m 999 (전에는 n 12 -- room 의 길이) 배열/슬라이스/str 일 때만 길이로 읽는다. fieldn.fe 가 이것과, 옆의 슬라이스가 여전히 길이로 답하는 것을 함께 고정한다. 222/222, 30/30. --- fec/src/lowerexp.c | 12 +++++++--- fec/tests/exec/fieldn.fe | 49 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 fec/tests/exec/fieldn.fe diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c index 40de08c..2b97662 100644 --- a/fec/src/lowerexp.c +++ b/fec/src/lowerexp.c @@ -151,9 +151,15 @@ Slot lower_expr_core(Lower *L, FeNode *n) return slot_place(fe_ir_at_temp(p, 0), it, ir_size(t)); } /* `.n` is how many elements there are, which an array knows at - compile time and a slice carries beside its pointer. */ - if (n->b && n->b->text && !strcmp(n->b->text, "n")) { - FeType *bt = n->a ? n->a->sem_type : 0; + compile time and a slice carries beside its pointer. Only for those: + a struct is free to have a field called `n`, and reading it as a + length would quietly hand back the wrong four bytes. */ + if (n->b && n->b->text && !strcmp(n->b->text, "n") && + n->a && n->a->sem_type && + (n->a->sem_type->kind == FE_TYPE_ARRAY || + n->a->sem_type->kind == FE_TYPE_SLICE || + n->a->sem_type->kind == FE_TYPE_STR)) { + FeType *bt = n->a->sem_type; Slot base; if (bt && bt->kind == FE_TYPE_ARRAY) return slot_value(fe_ir_const(L->m, L->b, FE_IR_I32, diff --git a/fec/tests/exec/fieldn.fe b/fec/tests/exec/fieldn.fe new file mode 100644 index 0000000..cfd148f --- /dev/null +++ b/fec/tests/exec/fieldn.fe @@ -0,0 +1,49 @@ +// EXIT:0 +// OUTPUT:n 777 m 999 +// OUTPUT:len 12 first 7 +// OUTPUT:count 3 +// OUTPUT:balanced +unit fieldn; + +import std.io; +import std.sys; +import std.mem; + +// `.n` on a slice is its length. On a struct it is whatever field is called +// `n` -- and a struct is allowed to call a field that. Reading one as the +// other hands back the four bytes beside the pointer, which is a plausible +// number and so goes unnoticed. + +struct Box { + room: ^[]mut u8, + n: usize, + m: usize, +} + +struct Counter { + n: usize, + + fn bump(self: &mut Self) -> void { self.n = self.n + 1; return; } +} + +fn run() -> !void { + let r: ^[]mut u8 = try mem.alloc_slice(u8, 12); + r.^[0] = 7; + let b: Box = Box{ room: r, n: 777, m: 999 }; + @print("n {} m {}\n", b.n, b.m); + // The slice beside it still answers with its length. + @print("len {} first {}\n", b.room.^.n, b.room.^[0]); + var c: Counter = Counter{ n: 0 }; + c.bump(); + c.bump(); + c.bump(); + @print("count {}\n", c.n); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} From 63ad48cd8a4dacf36fbaea7108304ab7f7107bb1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 12:56:34 +0900 Subject: [PATCH 164/184] =?UTF-8?q?=ED=8A=B8=EB=9E=A9=EC=9D=80=20=EA=B2=80?= =?UTF-8?q?=EC=82=AC=EA=B0=80=20=EC=93=B0=EC=9D=B8=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=EC=9D=84=20=EB=8C=84=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 빌드 전체가 한 모듈이라 파일 이름도 하나였다. std.list 안에서 터진 경계 검사가 프로그램의 파일 이름을 대고 있었으니, 줄 번호는 맞는데 파일이 틀려서 엉뚱한 줄을 가리켰다 -- 이름을 안 대는 것보다 나쁘다. 모듈이 파일 표를 들고 트랩은 그 인덱스를 든다. 생성기는 파일마다 FE_FILE_n 을 한 번씩 찍는다. before: index out of bounds at main.fe:2 after: index out of bounds at pick.fe:6 그리고 Parser.on 이 구조체 리터럴 안에서 다시 try 를 쓴다. 앞서 그것이 깨졌던 것은 try 때문이 아니라 Parser 가 1 바이트로 자리잡았기 때문이었다. 224/224, 31/31. --- fec/src/ir.c | 24 ++++++++++++++++++++---- fec/src/ir.h | 13 +++++++++++-- fec/src/lower.c | 13 ++++++++++--- fec/src/lowerpri.h | 1 + fec/src/lowerstm.c | 1 - fec/src/x86.c | 13 +++++++++---- fec/tests/exec/lexer/parse.fe | 4 +--- fec/tests/exec/trapsrc/main.fe | 20 ++++++++++++++++++++ fec/tests/exec/trapsrc/pick.fe | 7 +++++++ 9 files changed, 79 insertions(+), 17 deletions(-) create mode 100644 fec/tests/exec/trapsrc/main.fe create mode 100644 fec/tests/exec/trapsrc/pick.fe diff --git a/fec/src/ir.c b/fec/src/ir.c index 6b5f117..5ee0072 100644 --- a/fec/src/ir.c +++ b/fec/src/ir.c @@ -5,7 +5,7 @@ void fe_ir_module_init(FeIrModule *m) { fe_arena_init(&m->arena, 16384); - m->unit_file = ""; + m->file_count = 0; m->entry_main = 0; m->funcs = 0; m->last_func = 0; @@ -13,6 +13,20 @@ void fe_ir_module_init(FeIrModule *m) m->last_global = 0; } +/* The index of this path in the module's file table, adding it if it is new. + Traps carry the index rather than the string so the generator emits each + name once. */ +unsigned fe_ir_file(FeIrModule *m, const char *path) +{ + unsigned i; + if (!path) path = ""; + for (i = 0; i < m->file_count; ++i) + if (!strcmp(m->files[i], path)) return i; + if (m->file_count >= FE_IR_FILE_MAX) return 0; + m->files[m->file_count] = path; + return m->file_count++; +} + void fe_ir_module_destroy(FeIrModule *m) { fe_arena_destroy(&m->arena); @@ -301,13 +315,15 @@ void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value) b->has_ret_value = has_value; } -void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line) +void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line, + unsigned file) { if (b->terminated) return; b->terminated = 1; b->term = FE_IR_TRAP; b->trap = reason; b->trap_line = line; + b->trap_file = file; } const char *fe_ir_type_name(FeIrType t) @@ -432,8 +448,8 @@ void fe_ir_dump(const FeIrModule *m, FILE *out) const FeIrValue *v; const FeIrGlobal *g; unsigned i; - if (m->unit_file && m->unit_file[0]) - fprintf(out, "; unit file %s\n", m->unit_file); + for (i = 0; i < m->file_count; ++i) + fprintf(out, "; file %u %s\n", i, m->files[i]); for (g = m->globals; g; g = g->next) fprintf(out, "global @%s : %s %lu\n", g->name, fe_ir_type_name(g->type), g->size); diff --git a/fec/src/ir.h b/fec/src/ir.h index beff64f..8801844 100644 --- a/fec/src/ir.h +++ b/fec/src/ir.h @@ -4,6 +4,9 @@ #include "arena.h" #include +/* How many source files one build can trap from. */ +#define FE_IR_FILE_MAX 64 + /* The intermediate representation. `IR.md` is the description; this is the shape it takes in memory. @@ -88,6 +91,7 @@ typedef struct FeIrBlock { int has_ret_value; FeIrTrap trap; unsigned long trap_line; + unsigned trap_file; /* index into the module's file table */ /* Set once a terminator is chosen. Lowering asks before appending a jump, so a `return` inside a branch is not overwritten by the jump to the join block. */ @@ -142,7 +146,10 @@ typedef struct FeIrGlobal { typedef struct FeIrModule { FeArena arena; - const char *unit_file; /* the one file-name string a unit's traps share */ + /* Every unit in the build lands in one module, so a trap has to say which + file it came from rather than share one name with the whole program. */ + const char *files[FE_IR_FILE_MAX]; + unsigned file_count; /* The entry unit's `main`, if it has one. The runtime's start stub calls a fixed name, so the generator emits a jump to this one. */ const char *entry_main; @@ -152,6 +159,7 @@ typedef struct FeIrModule { FeIrGlobal *last_global; } FeIrModule; +unsigned fe_ir_file(FeIrModule *m, const char *path); void fe_ir_module_init(FeIrModule *m); void fe_ir_module_destroy(FeIrModule *m); @@ -198,7 +206,8 @@ void fe_ir_copy(FeIrModule *m, FeIrBlock *b, FeIrPlace dst, FeIrPlace src, void fe_ir_jmp(FeIrBlock *b, unsigned target); void fe_ir_br(FeIrBlock *b, unsigned cond, unsigned t, unsigned f); void fe_ir_ret(FeIrBlock *b, unsigned value, int has_value); -void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line); +void fe_ir_trap(FeIrBlock *b, FeIrTrap reason, unsigned long line, + unsigned file); void fe_ir_dump(const FeIrModule *m, FILE *out); const char *fe_ir_type_name(FeIrType t); diff --git a/fec/src/lower.c b/fec/src/lower.c index 41ff46e..dbfc439 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -199,6 +199,13 @@ FeIrBlock *new_block(Lower *L) return fe_ir_block(L->m, L->fn); } +/* Which file a trap raised right now came from. The whole build lowers into + one module, so the unit being lowered is the only thing that knows. */ +unsigned trap_file(Lower *L) +{ + return fe_ir_file(L->m, L->c->unit ? L->c->unit->path : ""); +} + /* A check that must hold. `ok` is a condition; when it is false the program stops where it is. `--no-checks` removes the comparison and the branch, not just the message, which is the whole point of the flag. */ @@ -208,7 +215,7 @@ void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line) FeIrBlock *cont = new_block(L); fe_ir_br(L->b, ok, cont->id, bad->id); L->b = bad; - fe_ir_trap(L->b, reason, line); + fe_ir_trap(L->b, reason, line, trap_file(L)); L->b = cont; } @@ -403,13 +410,13 @@ int lower_builtin(Lower *L, FeNode *n, Slot *out) const char *name = n->text; if (!name || name[0] != '@') return 0; if (!strcmp(name, "@trap")) { - fe_ir_trap(L->b, FE_TRAP_EXPLICIT, n->loc.line); + fe_ir_trap(L->b, FE_TRAP_EXPLICIT, n->loc.line, trap_file(L)); L->b = new_block(L); *out = slot_void(); return 1; } if (!strcmp(name, "@unreachable")) { - fe_ir_trap(L->b, FE_TRAP_UNREACHABLE, n->loc.line); + fe_ir_trap(L->b, FE_TRAP_UNREACHABLE, n->loc.line, trap_file(L)); L->b = new_block(L); *out = slot_void(); return 1; diff --git a/fec/src/lowerpri.h b/fec/src/lowerpri.h index 9ebd779..6b87c6b 100644 --- a/fec/src/lowerpri.h +++ b/fec/src/lowerpri.h @@ -121,6 +121,7 @@ unsigned declare_var(Lower *L, const char *cname, const FeType *t, int release_flag(Lower *L, unsigned local, unsigned *flag); LowerVar *find_var(Lower *L, const char *cname); FeIrBlock *new_block(Lower *L); +unsigned trap_file(Lower *L); void guard(Lower *L, unsigned ok, FeIrTrap reason, unsigned long line); FeIrType tag_type(const FeType *t); int uses_niche(const FeType *t); diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index df09f18..cfc5f5d 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -631,7 +631,6 @@ int fe_lower_program(FeCheck *c, FeIrModule *out) c->ast = &unit->ast; c->unit = unit; c->types.unit_name = unit->name[0] ? unit->name : "unit"; - if (!out->unit_file || !out->unit_file[0]) out->unit_file = unit->path; for (n = unit->ast.root ? unit->ast.root->children : 0; n; n = n->next) if (n->kind == FE_N_GLOBAL || n->kind == FE_N_CONST) lower_global(&L, n); diff --git a/fec/src/x86.c b/fec/src/x86.c index 98e5824..69315b7 100644 --- a/fec/src/x86.c +++ b/fec/src/x86.c @@ -548,7 +548,8 @@ static void emit_func(const FeIrModule *m, const FeIrFunc *f, FILE *out) break; case FE_IR_TRAP: fprintf(out, " push %lu\n", b->trap_line); - fprintf(out, " push offset FE_UNIT_FILE\n"); + fprintf(out, " push offset FE_FILE_%u\n", + b->trap_file); fprintf(out, " push %u\n", (unsigned)b->trap); fprintf(out, " call fe_trap\n"); fprintf(out, " add esp, 12\n"); @@ -587,6 +588,7 @@ void fe_x86_emit(const FeIrModule *m, FILE *out) const FeIrGlobal *g; int any_trap = 0; const FeIrBlock *b; + unsigned i; for (f = m->funcs; f && !any_trap; f = f->next) for (b = f->first; b; b = b->next) @@ -623,9 +625,12 @@ void fe_x86_emit(const FeIrModule *m, FILE *out) if (any_trap) fputs("extern fe_trap : near\n", out); fputs("\n_DATA segment dword public 'DATA'\n", out); - if (any_trap) { - fputs("public FE_UNIT_FILE\nFE_UNIT_FILE label byte\n", out); - emit_string(m->unit_file, out); + /* One name per file a trap can come from. A build is many units in + one module, and a trap that names the wrong file is worse than + one that names none. */ + for (i = 0; i < m->file_count; ++i) { + fprintf(out, "public FE_FILE_%u\nFE_FILE_%u label byte\n", i, i); + emit_string(m->files[i], out); } for (g = m->globals; g; g = g->next) { unsigned long i; diff --git a/fec/tests/exec/lexer/parse.fe b/fec/tests/exec/lexer/parse.fe index ffd5d2b..50f759d 100644 --- a/fec/tests/exec/lexer/parse.fe +++ b/fec/tests/exec/lexer/parse.fe @@ -23,10 +23,8 @@ pub struct Parser { pub errors: usize, pub fn on(src: []u8) -> !Self { - let nodes: list.List(ast.Node) = - try list.List(ast.Node).with_capacity(16); var p: Self = Self{ - nodes: nodes, + nodes: try list.List(ast.Node).with_capacity(16), at: 0, line: 1, cur: tok.Token{ kind: tok.Kind.End, from: 0, len: 0, line: 1 }, diff --git a/fec/tests/exec/trapsrc/main.fe b/fec/tests/exec/trapsrc/main.fe new file mode 100644 index 0000000..12f89d7 --- /dev/null +++ b/fec/tests/exec/trapsrc/main.fe @@ -0,0 +1,20 @@ +// EXIT:3 +// OUTPUT:index out of bounds +// OUTPUT:pick.fe:6 +// NOCHECKS:0 +unit main; + +import pick; + +// A build is many units in one module. A trap has to name the file the check +// was written in, not the file the program was started from. + +const S: str = "abc"; + +fn main() -> i32 { + let c: u8 = pick.at(S, 9); + // Without the checks the read runs off the end and `c` is whatever + // was there, so nothing is decided by its value. + if c == 0 { return 0; } + return 0; +} diff --git a/fec/tests/exec/trapsrc/pick.fe b/fec/tests/exec/trapsrc/pick.fe new file mode 100644 index 0000000..628d989 --- /dev/null +++ b/fec/tests/exec/trapsrc/pick.fe @@ -0,0 +1,7 @@ +unit pick; + +// The failing index is here, two units away from the program that runs it. + +pub fn at(s: []u8, i: usize) -> u8 { + return s[i]; +} From d1d031019aee84b9ddc100c537657eaa7875d36d Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 12:57:27 +0900 Subject: [PATCH 165/184] =?UTF-8?q?docs:=20TODO=20=EC=99=80=20fixture=20RE?= =?UTF-8?q?ADME=20=EB=A5=BC=20=EC=A7=80=EA=B8=88=20=EC=83=81=ED=83=9C?= =?UTF-8?q?=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파서까지 끝났으니 남은 길을 다시 적는다. 필드 단위 대여가 목록에 올라왔다 -- map 도 파서도 같은 자리에서 걸렸다. 224/224, 31/31. --- TODO.md | 24 ++++++++++++++---------- fec/tests/exec/lexer/README.md | 15 +++++++++++++++ fec/tests/exec/trapsrc/README.md | 5 +++++ 3 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 fec/tests/exec/lexer/README.md create mode 100644 fec/tests/exec/trapsrc/README.md diff --git a/TODO.md b/TODO.md index ccd4e50..d91c847 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,8 @@ # TODO ``` -uv run python tests/run.py 217/217 컴파일러가 프로그램에 대해 뭐라고 하는가 -uv run python tests/exec.py 27/27 컴파일된 프로그램이 실제로 무엇을 하는가 +uv run python tests/run.py 224/224 컴파일러가 프로그램에 대해 뭐라고 하는가 +uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 무엇을 하는가 ``` ``` @@ -22,10 +22,12 @@ uv run python tests/exec.py 27/27 컴파일된 프로그램이 실제로 | `mem.Arena` | R11 이 말하는 아레나 + 핸들이 실제로 쓸 수 있다 | | 레지스터 할당 | 블록 단위 선형 스캔. calc 4437 → 3796 줄 (-14%) | | **Ferro 렉서를 Ferro 로** | 강제 함수. 돌아간다 | +| `std.map` | 키를 맵이 소유한 버퍼에 복사하고 슬롯은 위치만 든다 | +| **Ferro 파서를 Ferro 로** | 노드 배열 하나 + 인덱스. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 묶인다 | -렉서가 알려준 것: **R11 의 모양(아레나가 소유하고 인덱스가 가리킨다)은 쓸 수 -있다.** 토큰이 `from`/`len` 을 들고 소스가 옆에서 같이 다니는 것은 장황하지만 -막히지 않는다. 모든 함수가 `src` 를 하나 더 받는 것이 값이다. +파서가 알려준 것: **자기 참조 자료구조는 인덱스로 짓는다.** 노드는 `^Node` 를 +들 수 없고(자식이 여럿이며 한 번씩 소유하지 않는다) `&Node` 도 들 수 없다(R4). +소유자 하나와 인덱스 여럿이 남는 유일한 모양이고, 그것으로 충분했다. --- @@ -33,10 +35,10 @@ uv run python tests/exec.py 27/27 컴파일된 프로그램이 실제로 | # | 일 | 규모 | 비고 | |---|---|---|---| -| 1 | Ferro 파서를 Ferro 로 | 대 | 렉서 다음. AST 를 아레나 + 핸들로 짓는다 | -| 2 | `std.map` | 중 | 심볼 표에 필요. 지금은 `List` 선형 탐색뿐 | -| 3 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | -| 4 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | +| 1 | 심볼 표와 이름 해석을 Ferro 로 | 중 | `std.map` 이 준비됐다 | +| 2 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | +| 3 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | +| 4 | 필드 단위 대여 | 중 | 지금은 루트 단위라 `self` 의 한 필드에 쓰는 동안 다른 필드를 못 읽는다 | | 5 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | ## 언어에 남은 구멍 @@ -48,6 +50,7 @@ uv run python tests/exec.py 27/27 컴파일된 프로그램이 실제로 | `@sprint` | 전개하지 않는다. `@print`/`@fprint` 만 | | `interrupt` `shared` `atomic` `critical` | 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | | lowering 미구현 진단 | `internal: cannot lower X`. 사용자 오류처럼 보이지 않는다 | +| 배열·enum 페이로드의 자동 해제 | `release_at` 은 구조체 필드까지만 내려간다 | --- @@ -71,10 +74,11 @@ uv run python tests/exec.py 27/27 컴파일된 프로그램이 실제로 | `usize`/`isize` | **타깃의 포인터 폭.** 비트 수를 약속하지 않아 64비트 문이 닫히지 않음 | | 제네릭 | 모노모피제이션. 순수 프론트엔드 기능이라 IR 에 제네릭 개념이 없음 | | 덩어리 전달 | 전부 주소로. ISA 마다 다른 구조체 전달 ABI 를 피해감 | -| 트랩 | `trap ` → `fe_trap(reason, UNIT_FILE, line)` | +| 트랩 | `trap ` → `fe_trap(reason, FE_FILE_n, line)`. 파일은 검사가 쓰인 유닛 | | 슬라이스 배치 | 포인터 다음 길이. 오프셋은 `lowerpri.h` 한 군데에만 | | 오류 코드 | `error.Name` 을 빌드 전체에서 모아 철자 순으로 1부터 | | 레지스터 | ebx·esi·edi 를 블록 안에 머무는 임시값에 준다. eax/ecx/edx 는 스크래치 | +| 해제 | 소유자를 놓으면 그것이 가진 것도 놓는다 (R1). `drop` 을 부른 뒤 필드로 내려간다 — 그래서 `List`/`Arena`/`Map` 은 `drop` 이 없다 | --- diff --git a/fec/tests/exec/lexer/README.md b/fec/tests/exec/lexer/README.md new file mode 100644 index 0000000..176c2c5 --- /dev/null +++ b/fec/tests/exec/lexer/README.md @@ -0,0 +1,15 @@ +# Ferro 로 쓴 Ferro 프런트엔드 + +셀프호스팅의 강제 함수. 언어가 자기 컴파일러를 쓸 만한지는 써 봐야 안다. + +| 파일 | 무엇 | +|---|---| +| `tok.fe` | 토큰. 텍스트를 들지 않고 소스 안의 위치를 든다 (R4) | +| `scan.fe` | 렉서. `next(src, &mut at, &mut line)` | +| `main.fe` | 렉서 프로그램. 종류별로 세고 몇 개를 찍는다 | +| `ast.fe` | 노드. 자식은 노드 배열 안의 인덱스다 | +| `parse.fe` | 재귀 하강 파서. 토큰 하나를 앞서 본다 | +| `tree.fe` | 파서 프로그램. 식을 전위 표기로 다시 찍는다 | + +전위 표기로 찍는 것이 요점이다. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 나오는 것 +말고는 우선순위가 맞았는지 볼 방법이 없다. diff --git a/fec/tests/exec/trapsrc/README.md b/fec/tests/exec/trapsrc/README.md new file mode 100644 index 0000000..e8d0803 --- /dev/null +++ b/fec/tests/exec/trapsrc/README.md @@ -0,0 +1,5 @@ +# 트랩이 대는 파일 + +빌드 전체가 한 모듈이 되므로 트랩은 자기가 어느 유닛에서 왔는지 스스로 말해야 +한다. 여기서는 경계 검사가 `pick.fe` 에 있고 프로그램은 `main.fe` 다. 트랩이 +`main.fe` 를 대면 줄 번호가 맞아도 엉뚱한 줄을 가리킨다. From 120a36eef59f822d681047c69aa1ea8c8068defb Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 15:16:24 +0900 Subject: [PATCH 166/184] =?UTF-8?q?spec:=20=EC=99=84=ED=99=94=ED=96=88?= =?UTF-8?q?=EB=8D=98=20=EC=9D=B4=EB=8F=99=20=EA=B7=9C=EC=B9=99=20=EB=91=98?= =?UTF-8?q?=EC=9D=84=20=EB=AA=85=EC=84=B8=EC=97=90=20=EB=84=A3=EB=8A=94?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구현만 알고 있으면 둘 중 하나가 틀린 것이다. - 배타 대여를 호출에 넘기는 것은 재대여 -- 이미 §4.2 에 있었다. - 자기 drop 안의 부분 이동은 R7 예외다. 객체가 사라지는 중이라 drop 이 돌아간 뒤에 그 반쪽짜리 값을 읽을 코드가 없다. 다른 함수에는 예외가 없다. TODO 의 '판단을 기다리는 것' 이 비었다. --- SPEC.md | 2 ++ TODO.md | 14 ++------------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/SPEC.md b/SPEC.md index 6c8e6c8..0bd8ba1 100644 --- a/SPEC.md +++ b/SPEC.md @@ -280,6 +280,8 @@ x += 1; // OK — 여기서 r의 대여는 이미 끝났다 **R7 (참조 무효화와 부분 이동).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. own.c는 변수 단위 상태만 추적하므로 field/index/`.?` projection에서 비-Copy 소유값을 이동해 꺼내는 것은 금지한다. `mem.replace(&mut place, replacement)`로 유효한 대체값을 남기면서 꺼내야 한다. 배열의 선택적 소유 원소는 `?^T`로 두고 `mem.replace(&mut arr[i], null).?`로 꺼낸다. projection chain 자체(`p.?.^`, `s.field.x`)는 값을 소비하지 않는다. +이 금지에는 예외가 하나 있다. **타입 자신의 `drop` 안에서는 `self`의 projection에서 값을 꺼낼 수 있다.** 그 객체는 사라지는 중이고 `drop`이 돌아간 뒤에 그것을 읽을 수 있는 코드가 없으므로, R7이 막으려는 "뒤에 남은 반쪽짜리 값"이 생기지 않는다. 다른 함수에서는 예외가 없다. + **R8 (파생 반환).** 함수는 다음 두 경우에 한해 `&T`, `&mut T`, `[]T`, `[]mut T`와 이를 `?`로 감싼 타입을 반환할 수 있다. **(a) 파라미터 파생.** 메서드는 파생 원본이 항상 참조성 `self`여야 한다. 다른 참조성 인자를 추가로 받을 수 있지만 반환값은 그 인자에서 파생될 수 없고 그 인자의 임시 대여는 문장 끝에 풀린다. 자유 함수는 참조성 파라미터(`&T`, `&mut T`, `[]T`, `[]mut T`)가 **정확히 하나**여야 한다. 두 경우 모두 반환값이 정해진 원본에서 파생됐음을 컴파일러가 함수 본문만 보고 확인한다. 파생은 슬라이싱, 인덱싱, 필드 접근, projection, `&`/`&mut` 취함과 다른 R8(a) 호출의 연쇄다. 반환의 가변성은 원본 이하여야 한다. diff --git a/TODO.md b/TODO.md index d91c847..d3e546f 100644 --- a/TODO.md +++ b/TODO.md @@ -54,18 +54,6 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 --- -## 네 판단을 기다리는 것 - -세션 중에 **이동 규칙 두 곳을 완화**했다. R4(참조는 집합 저장소에 못 들어감)는 -건드리지 않았다. - -| | 무엇 | 왜 | 대안 | -|---|---|---|---| -| 1 | `&mut T` 를 `&mut T` 파라미터에 넘기는 것은 이동이 아니라 **호출 동안의 재대여** | 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번뿐이다. 렉서도 계산기도 이것 없이는 못 쓴다 | 되돌리면 재귀 하강 파서를 못 쓴다 | -| 2 | 자기 `drop` 안에서는 필드를 꺼낼 수 있다 (R7 예외) | 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다 | `drop(self: Self)` 로 값을 소비하게 바꾸면 R7 을 안 건드려도 된다 | - ---- - ## 정해진 것 | | | @@ -79,6 +67,8 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | 오류 코드 | `error.Name` 을 빌드 전체에서 모아 철자 순으로 1부터 | | 레지스터 | ebx·esi·edi 를 블록 안에 머무는 임시값에 준다. eax/ecx/edx 는 스크래치 | | 해제 | 소유자를 놓으면 그것이 가진 것도 놓는다 (R1). `drop` 을 부른 뒤 필드로 내려간다 — 그래서 `List`/`Arena`/`Map` 은 `drop` 이 없다 | +| 배타 대여를 호출에 넘기기 | **이동이 아니라 호출 동안의 재대여** (SPEC §4.2, §5 R6). 없으면 배타 파라미터를 함수당 한 번만 넘길 수 있어 재귀 하강 파서를 못 쓴다 | +| 자기 `drop` 안의 부분 이동 | **허용** (SPEC §5 R7 예외). 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다 | --- From f7e667652e9897ded6b5e16dc6d7ea2f85105d61 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 15:29:12 +0900 Subject: [PATCH 167/184] =?UTF-8?q?=EB=8C=80=EC=97=AC=EB=8A=94=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=EA=B0=80=20=EC=95=84=EB=8B=88=EB=9D=BC=20place=20?= =?UTF-8?q?=EB=8B=A8=EC=9C=84=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit p.a 와 p.b 는 서로 다른 자리인데 한쪽을 대여하면 다른 쪽까지 잠겼다. 메서드가 하는 일의 대부분이 한 필드에 쓰면서 다른 필드를 읽는 것이라, std.map 의 keep 은 그것 때문에 함수 둘로 갈라져 있었고 파서도 같은 자리에서 걸렸다. FeOwnState 가 필드별 칸을 넷 갖는다. 값으로 복사되는 구조체라 흐름 병합과 스냅샷은 손댈 것이 없었다. 전체를 대여하면 모든 필드와 충돌하고, 필드를 대여하면 전체 및 같은 필드와 충돌한다. 칸이 모자라면 전체 대여로 되돌아가 더 많이 보고할 뿐 놓치지 않는다. 읽기는 식별자에서 일어나는데 그 자리에서는 자기가 무엇의 밑동인지 알 수 없다. 그래서 투영이 내려가는 길에 어느 필드인지 적어두고 식별자가 그것을 집는다. 인덱스는 갈라지지 않는다 -- xs[i] 의 i 는 상수가 아닐 수 있고, 필드 이름은 상수다. 길에서 나온 것: mem.replace 가 목적지 대여를 가져가고 돌려주지 않았다. 일반 호출 인자는 문장 끝에 돌려주는데 intrinsic 경로에만 그것이 없었다. 전에는 그 자리가 어차피 거부돼서 드러나지 않았다. var p = Pair{ a: 1, b: 2 }; let r = &mut p.a; p.b = 3; // ok -- 전에는 에러 p.a = 3; // 에러 take(p); // 에러 SPEC §5 R6 을 고쳤고, 옛 규칙을 그대로 적어둔 문단과 예제를 갈아치웠다. own/badrfld 는 이제 허용되는 코드였으므로 같은 필드를 건드리도록 다시 겨눴고 okrfld·badrall·badrsame·exec/fieldbrw 를 더했다. 228/228, 32/32. --- SPEC.md | 10 +- TODO.md | 5 +- fec/src/check.c | 65 +++++++++-- fec/src/checkcal.c | 36 ++++++ fec/src/checkexp.c | 9 +- fec/src/checkpri.h | 9 ++ fec/src/checkstm.c | 2 + fec/src/own.c | 226 +++++++++++++++++++++++++++++++++++++ fec/src/own.h | 26 +++++ fec/std/map.fe | 39 +++---- fec/tests/exec/fieldbrw.fe | 81 +++++++++++++ fec/tests/own/badrall.fe | 14 +++ fec/tests/own/badrfld.fe | 2 +- fec/tests/own/badrsame.fe | 12 ++ fec/tests/own/okrfld.fe | 14 +++ 15 files changed, 510 insertions(+), 40 deletions(-) create mode 100644 fec/tests/exec/fieldbrw.fe create mode 100644 fec/tests/own/badrall.fe create mode 100644 fec/tests/own/badrsame.fe create mode 100644 fec/tests/own/okrfld.fe diff --git a/SPEC.md b/SPEC.md index 0bd8ba1..150c909 100644 --- a/SPEC.md +++ b/SPEC.md @@ -251,18 +251,20 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 **R6 (배타성).** `&mut x`가 살아있는 동안 `x`에 대한 다른 참조 생성, 직접 읽기/쓰기, 이동이 금지된다. `&x`(공유)는 여러 개 동시 가능하지만 그동안 `x`에 쓰기/이동 금지. -v0.1의 대여 상태는 **root local/parameter 단위**로 추적한다. projection은 place의 root를 찾는 데만 쓰며 field-sensitive/index-sensitive 독립성을 증명하지 않는다. 따라서 `&mut p.a`는 `p` 전체를 배타 대여하고 그동안 `p.b`의 읽기·쓰기·대여도 금지한다. `&mut xs[0]`과 `&mut xs[1]`도 서로 다른 index라는 이유로 분리하지 않고 같은 root `xs`의 충돌 대여로 본다. 이는 compiler A/B의 함수-local 상태 기계를 작고 결정적으로 유지하기 위한 v0.1의 의도적인 보수성이다. +여기서 `x`는 변수가 아니라 **place**다. `p.a`와 `p.b`는 서로 다른 place이므로 한쪽을 대여해도 다른 쪽은 그대로 읽고 쓸 수 있다. 대여가 필드 단위로 갈라지는 것은 루트 변수의 직속 필드 한 겹까지이며, 그 아래(`p.a.b`)와 인덱스(`arr[i]`)·역참조(`p.^`)는 전체를 대여한 것으로 본다. 전체를 대여하면 모든 필드와 충돌하고, 필드를 대여하면 전체 및 같은 필드와 충돌한다. 한 값에서 동시에 갈라둘 수 있는 필드 수에는 구현 상한이 있고, 넘으면 전체 대여로 되돌아간다 — 더 많이 보고할 뿐 놓치지는 않는다. + +대여 상태는 **root local/parameter 와 그 직속 필드** 단위로 추적한다. `&mut xs[0]`과 `&mut xs[1]`은 서로 다른 index라는 이유로 분리하지 않고 같은 root `xs`의 충돌 대여로 본다 — index는 상수가 아닐 수 있고, 그것을 따지는 것은 함수-local 상태 기계가 감당할 일이 아니다. 필드 이름은 상수라 그 문제가 없으므로 갈라진다. ```fe var p = Pair{ a: 1, b: 2 }; let r = &mut p.a; -p.b = 3; // 에러: root p가 배타 대여 중 +p.b = 3; // ok: p.b는 다른 place +p.a = 3; // 에러: p.a가 배타 대여 중 +take(p); // 에러: 전체는 대여된 필드를 포함한다 r.^ = 4; let a = &mut xs[0]; let b = &mut xs[1]; // 에러: 둘 다 root xs를 대여 -a.^ = 1; -b.^ = 2; ``` 참조의 생존 구간은 **참조 변수의 마지막 사용 지점까지**다. 그 이후에는 원본에 대한 접근·이동이 다시 허용된다. 조건부 흐름에서는 모든 경로의 마지막 사용 중 가장 나중 지점을 취한다. 임시 참조(`f(&x)`)는 그 문장 끝까지다. `defer` 블록에서 사용한 참조와 그 원본의 대여는 해당 defer가 실행되는 스코프 끝까지 연장한다. diff --git a/TODO.md b/TODO.md index d3e546f..99bdcea 100644 --- a/TODO.md +++ b/TODO.md @@ -24,6 +24,7 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | **Ferro 렉서를 Ferro 로** | 강제 함수. 돌아간다 | | `std.map` | 키를 맵이 소유한 버퍼에 복사하고 슬롯은 위치만 든다 | | **Ferro 파서를 Ferro 로** | 노드 배열 하나 + 인덱스. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 묶인다 | +| 필드 단위 대여 | `p.a` 와 `p.b` 는 다른 place 다. `std.map` 의 `keep` 이 다시 함수 하나가 됐다 | 파서가 알려준 것: **자기 참조 자료구조는 인덱스로 짓는다.** 노드는 `^Node` 를 들 수 없고(자식이 여럿이며 한 번씩 소유하지 않는다) `&Node` 도 들 수 없다(R4). @@ -38,8 +39,7 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | 1 | 심볼 표와 이름 해석을 Ferro 로 | 중 | `std.map` 이 준비됐다 | | 2 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | | 3 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | -| 4 | 필드 단위 대여 | 중 | 지금은 루트 단위라 `self` 의 한 필드에 쓰는 동안 다른 필드를 못 읽는다 | -| 5 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | +| 4 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | ## 언어에 남은 구멍 @@ -69,6 +69,7 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | 해제 | 소유자를 놓으면 그것이 가진 것도 놓는다 (R1). `drop` 을 부른 뒤 필드로 내려간다 — 그래서 `List`/`Arena`/`Map` 은 `drop` 이 없다 | | 배타 대여를 호출에 넘기기 | **이동이 아니라 호출 동안의 재대여** (SPEC §4.2, §5 R6). 없으면 배타 파라미터를 함수당 한 번만 넘길 수 있어 재귀 하강 파서를 못 쓴다 | | 자기 `drop` 안의 부분 이동 | **허용** (SPEC §5 R7 예외). 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다 | +| 대여 단위 | **place 단위** (SPEC §5 R6). 루트 변수의 직속 필드 한 겹까지 갈라지고, 그 아래와 인덱스·역참조는 전체 대여다. 한 값당 4 필드까지, 넘으면 전체로 되돌아간다 | --- diff --git a/fec/src/check.c b/fec/src/check.c index 57ed1cf..357b955 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -260,6 +260,7 @@ FeSym *add_symbol(FeCheckerState *s, FeScope *scope, sym->decl = decl; fe_own_state_init(&sym->own, initialized); sym->borrow_root = 0; + sym->borrow_field = 0; sym->borrow_mut = 0; sym->borrow_defer = 0; sym->owner = scope; @@ -506,9 +507,47 @@ int own_is_global(FeCheckerState *s, FeSym *sym) return 0; } +/* Strip the `&`/`&mut` off an expression; the place underneath is what is + being reached. */ +static FeNode *own_strip_ref(FeNode *e) +{ + while (e && e->kind==FE_N_UNARY && e->text && + (strcmp(e->text,"&")==0 || strcmp(e->text,"&mut")==0)) + e=e->a; + return e; +} + +/* The first field projected off the root of `expr`, and that root. + `self.bytes.^[i]` projects `bytes` off `self`. An index (`arr[i]`) and a + dereference (`p.^`) name no field, so they answer for the whole value -- + which is what the checker did for everything before. */ +const char *own_projected_field(FeNode *expr, FeNode **root_out) +{ + FeNode *inner; + FeNode *outer=0; + if (root_out) *root_out=0; + inner=own_strip_ref(expr); + while (inner && (inner->kind==FE_N_MEMBER || inner->kind==FE_N_INDEX)) { + outer=inner; + inner=own_strip_ref(inner->a); + } + if (!inner || inner->kind!=FE_N_IDENT || !outer) return 0; + if (outer->kind!=FE_N_MEMBER) return 0; + /* A member node's own text is the token the postfix chain started at, not + the operator, so the spelling of the projection is what to look at: + `.?` carries nothing on the right and `.^` carries a caret. */ + if (outer->text && (strcmp(outer->text,".?")==0 || + strcmp(outer->text,".^")==0)) return 0; + if (!outer->b || !outer->b->text) return 0; + if (strcmp(outer->b->text,"^")==0) return 0; + if (root_out) *root_out=inner; + return outer->b->text; +} + void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) { FeSym *root=own_root_symbol(s,expr); + const char *field=own_projected_field(expr,0); if (!root) return; if (mutable && root->type && root->type->kind==FE_TYPE_REF && !root->type->ref_mut) { @@ -521,9 +560,9 @@ void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable) err(s->c,expr->loc,"cannot borrow a mutable global"); return; } - fe_own_access(s->c->diags,&root->own, - mutable ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, - expr->loc); + fe_own_access_field(s->c->diags,&root->own,field, + mutable ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + expr->loc); } void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) @@ -533,8 +572,12 @@ void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr) if (strcmp(expr->text,"&")!=0 && strcmp(expr->text,"&mut")!=0) return; root=own_root_symbol(s,expr->a); if (!root) return; - if (strcmp(expr->text,"&mut")==0) fe_own_release_exclusive(&root->own); - else fe_own_release_shared(&root->own); + { + const char *field=own_projected_field(expr->a,0); + if (strcmp(expr->text,"&mut")==0) + fe_own_release_exclusive_field(&root->own,field); + else fe_own_release_shared_field(&root->own,field); + } } /* Return-reference provenance is represented at call sites by retaining a @@ -577,6 +620,7 @@ void own_bind_derived_call(FeCheckerState *s, FeSym *binding, else fe_own_access(s->c->diags,&root->own,FE_OWN_BORROW_SHARED,value->loc); binding->borrow_root=root; + binding->borrow_field=0; binding->borrow_mut=value->sem_type->kind==FE_TYPE_REF && value->sem_type->ref_mut; } @@ -631,9 +675,13 @@ void own_release_after_stmt(FeCheckerState *s, FeScope *scope, ref->decl && ref->decl->text ? ref->decl->text : ref->name); if (!scope_end && (ref->borrow_defer || !last || last->defer_extended || !own_contains_node(stmt,last->last_node))) continue; - if (ref->borrow_mut) fe_own_release_exclusive(&ref->borrow_root->own); - else fe_own_release_shared(&ref->borrow_root->own); + if (ref->borrow_mut) + fe_own_release_exclusive_field(&ref->borrow_root->own, + ref->borrow_field); + else fe_own_release_shared_field(&ref->borrow_root->own, + ref->borrow_field); ref->borrow_root=0; + ref->borrow_field=0; } } @@ -687,6 +735,7 @@ void flow_borrow_capture(FeFlowSlot *slots, FeFlowBorrow *states, if (!states) return; for (i=0;iborrow_root; + states[i].field=slots[i].sym->borrow_field; states[i].mutable=slots[i].sym->borrow_mut; } } @@ -698,6 +747,7 @@ void flow_borrow_restore(FeFlowSlot *slots, FeFlowBorrow *states, if (!states) return; for (i=0;iborrow_root=states[i].root; + slots[i].sym->borrow_field=states[i].field; slots[i].sym->borrow_mut=states[i].mutable; } } @@ -709,6 +759,7 @@ void flow_borrow_merge(FeFlowSlot *slots, FeFlowBorrow *left, if (!left || !right) return; for (i=0;iborrow_root=left[i].root ? left[i].root : right[i].root; + slots[i].sym->borrow_field=left[i].root ? left[i].field : right[i].field; slots[i].sym->borrow_mut=left[i].mutable || right[i].mutable; } } diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index f24f9e8..8454791 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -139,6 +139,7 @@ FeType *check_call(FeCheckerState *s, FeNode *n) !m7_actual_compatible(expected,b,value)) err(c,value->loc,"mem.replace value type mismatch"); mark_moved(s,value,value->sem_type ? value->sem_type : b); + own_release_temporary_borrow(s,arg); n->sem_type=expected ? expected : unknown(c); fe_type_require_replace(&c->types,n->sem_type); return n->sem_type; @@ -351,7 +352,24 @@ FeType *m7_check_lazy(FeCheckerState *s, FeNode *n, return payload; } +static FeType *check_expr_dispatch(FeCheckerState *s, FeNode *n); + +/* Every expression goes through here, which is where a projection can tell + the identifier underneath it which field is actually being reached. */ FeType *check_expr(FeCheckerState *s, FeNode *n) +{ + const char *save_field=s->proj_field; + FeNode *save_base=s->proj_base; + FeType *t; + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX)) + s->proj_field=own_projected_field(n,&s->proj_base); + t=check_expr_dispatch(s,n); + s->proj_field=save_field; + s->proj_base=save_base; + return t; +} + +static FeType *check_expr_dispatch(FeCheckerState *s, FeNode *n) { FeType *a; FeType *b; @@ -486,7 +504,24 @@ FeType *check_expr(FeCheckerState *s, FeNode *n) return check_expr_core(s,n); } +static FeType *check_lvalue_dispatch(FeCheckerState *s, FeNode *n, int read); + +/* An assignment target is a projection too, so it leaves the same word for the + identifier underneath: `self.room = x` reaches `room` and nothing else. */ FeType *check_lvalue(FeCheckerState *s, FeNode *n, int read) +{ + const char *save_field=s->proj_field; + FeNode *save_base=s->proj_base; + FeType *t; + if (n && (n->kind==FE_N_MEMBER || n->kind==FE_N_INDEX)) + s->proj_field=own_projected_field(n,&s->proj_base); + t=check_lvalue_dispatch(s,n,read); + s->proj_field=save_field; + s->proj_base=save_base; + return t; +} + +static FeType *check_lvalue_dispatch(FeCheckerState *s, FeNode *n, int read) { FeType *base=0; FeFieldType *field; @@ -786,6 +821,7 @@ void m7_check_decl_stmt(FeCheckerState *s, FeNode *n, int mutable) if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); sym->borrow_mut=strcmp(n->b->text,"&mut")==0; sym->borrow_defer=s->defer_depth!=0 || own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index a1b3bb5..3554e31 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -308,7 +308,10 @@ FeType *check_identifier(FeCheckerState *s, FeNode *n) n->cname = sym->cname; n->sem_type = sym->type; if (!sym->fn) { - fe_own_access(s->c->diags,&sym->own,FE_OWN_READ,n->loc); + /* When this identifier is the base of a projection, the read reaches + one field and not the whole value. The chain above left word. */ + const char *field = s->proj_base==n ? s->proj_field : 0; + fe_own_access_field(s->c->diags,&sym->own,field,FE_OWN_READ,n->loc); sym->moved=sym->own.move; } return sym->type; @@ -482,6 +485,10 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) if(a && a->kind==FE_TYPE_REF && !compatible(a->elem,b,value)) err(c,value->loc,"mem.replace value type mismatch"); if(value) mark_moved(s,value,b); + /* The destination is lent for the length of the call, the same + as any other argument. Without this the borrow stays live to + the end of the function and the place can never be read. */ + own_release_temporary_borrow(s,arg); n->sem_type=a && a->kind==FE_TYPE_REF ? a->elem : unknown(c); fe_type_require_replace(&c->types,n->sem_type); return n->sem_type; diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h index fa75c95..531045c 100644 --- a/fec/src/checkpri.h +++ b/fec/src/checkpri.h @@ -30,6 +30,8 @@ struct FeSym { release the root borrow without a separate alias engine. */ FeOwnState own; FeSym *borrow_root; + /* Which field of the root this binding borrowed, or null for all of it. */ + const char *borrow_field; int borrow_mut; int borrow_defer; FeScope *owner; @@ -52,6 +54,11 @@ typedef struct FeCheckerState { unsigned defer_depth; FeOwnLiveness liveness; FeNode *fn_node; + /* While a projection is being checked, which field of which base it + reaches. The read happens down at the identifier, which cannot see the + chain above it, so the chain leaves word here on the way down. */ + const char *proj_field; + FeNode *proj_base; } FeCheckerState; /* The type bindings in force, saved across a nested instantiation. */ @@ -70,6 +77,7 @@ typedef struct FeFlowSlot { typedef struct FeFlowBorrow { FeSym *root; + const char *field; int mutable; } FeFlowBorrow; @@ -114,6 +122,7 @@ void flow_restore(FeFlowSlot *slots, unsigned count); void flow_merge(FeFlowSlot *base, FeFlowSlot *left, FeFlowSlot *right, unsigned count); FeSym *own_root_symbol(FeCheckerState *s, FeNode *expr); +const char *own_projected_field(FeNode *expr, FeNode **root_out); int own_is_global(FeCheckerState *s, FeSym *sym); void own_borrow_expr(FeCheckerState *s, FeNode *expr, int mutable); void own_release_temporary_borrow(FeCheckerState *s, FeNode *expr); diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 4d28c1e..98f4d01 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -272,6 +272,7 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); sym->borrow_mut=strcmp(n->b->text,"&mut")==0; sym->borrow_defer=s->defer_depth != 0 || own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); @@ -297,6 +298,7 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) if (sym && n->b && n->b->kind==FE_N_UNARY && n->b->text && (strcmp(n->b->text,"&")==0 || strcmp(n->b->text,"&mut")==0)) { sym->borrow_root=own_root_symbol(s,n->b->a); + sym->borrow_field=own_projected_field(n->b->a,0); sym->borrow_mut=strcmp(n->b->text,"&mut")==0; sym->borrow_defer=s->defer_depth != 0 || own_defer_uses(s->fn_node ? s->fn_node->c : 0,n->text); diff --git a/fec/src/own.c b/fec/src/own.c index 8b51c37..7217289 100644 --- a/fec/src/own.c +++ b/fec/src/own.c @@ -92,6 +92,7 @@ int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place) void fe_own_state_init(FeOwnState *state, int initialized) { + unsigned i; if (!state) return; state->move = FE_OWN_AVAILABLE; state->initialized = initialized != 0; @@ -100,6 +101,12 @@ void fe_own_state_init(FeOwnState *state, int initialized) state->borrow_conflict = 0; state->move_loc = fe_own_no_loc(); state->borrow_loc = fe_own_no_loc(); + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + state->fields[i].name = 0; + state->fields[i].shared = 0; + state->fields[i].exclusive = 0; + state->fields[i].loc = fe_own_no_loc(); + } } static int fe_own_require_value(FeDiags *diags, FeOwnState *state, FeLoc loc) @@ -130,8 +137,187 @@ static int fe_own_require_stable_borrow(FeDiags *diags, FeOwnState *state, return 0; } +/* Whole-value state only: what a field access has to get past before it looks + at its own entry. `check` reports and decides; `apply` also records. */ +static int fe_own_access_whole(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); +static int fe_own_access_whole_check(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc); + +/* The entry for this field, or null. `make` asks for one to be created. */ +static FeOwnField *fe_own_field_slot(FeOwnState *state, const char *field, + int make) +{ + unsigned i; + unsigned free_slot = FE_OWN_FIELD_MAX; + if (!state || !field) return 0; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + if (state->fields[i].name && + strcmp(state->fields[i].name, field) == 0) return &state->fields[i]; + if (!state->fields[i].name && free_slot == FE_OWN_FIELD_MAX) + free_slot = i; + } + if (!make || free_slot == FE_OWN_FIELD_MAX) return 0; + state->fields[free_slot].name = field; + state->fields[free_slot].shared = 0; + state->fields[free_slot].exclusive = 0; + state->fields[free_slot].loc = fe_own_no_loc(); + return &state->fields[free_slot]; +} + +/* A live borrow of some field, for the accesses that reach the whole value. */ +static const FeOwnField *fe_own_field_live(const FeOwnState *state, + int mut_only) +{ + unsigned i; + if (!state) return 0; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *f = &state->fields[i]; + if (!f->name) continue; + if (f->exclusive) return f; + if (!mut_only && f->shared) return f; + } + return 0; +} + +void fe_own_release_shared_field(FeOwnState *state, const char *field) +{ + FeOwnField *f = fe_own_field_slot(state, field, 0); + if (!f || !f->shared) { fe_own_release_shared(state); return; } + --f->shared; + if (!f->shared && !f->exclusive) f->name = 0; +} + +void fe_own_release_exclusive_field(FeOwnState *state, const char *field) +{ + FeOwnField *f = fe_own_field_slot(state, field, 0); + if (!f || !f->exclusive) { fe_own_release_exclusive(state); return; } + f->exclusive = 0; + if (!f->shared) f->name = 0; +} + int fe_own_access(FeDiags *diags, FeOwnState *state, FeOwnAccessKind access, FeLoc loc) +{ + return fe_own_access_field(diags, state, 0, access, loc); +} + +int fe_own_access_field(FeDiags *diags, FeOwnState *state, const char *field, + FeOwnAccessKind access, FeLoc loc) +{ + FeOwnField *f; + const FeOwnField *other; + if (!state) return 0; + if (access == FE_OWN_PROJECTION) return 1; + if (!field) { + /* Reaching the whole value: a borrow of any part of it is in the way. + A shared borrow of a field still lets the whole be read. */ + other = fe_own_field_live(state, access == FE_OWN_READ); + if (other) { + fe_own_error_note(diags, loc, + access == FE_OWN_WRITE ? "cannot write while value is borrowed" : + access == FE_OWN_MOVE ? "cannot move while value is borrowed" : + access == FE_OWN_READ ? + "cannot read directly while value is mutably borrowed" : + "cannot borrow while a field of the value is borrowed", + other->loc, "borrow originated here"); + return 0; + } + return fe_own_access_whole(diags, state, access, loc); + } + /* Reaching one field: a borrow of the whole value is in the way, and so is + a borrow of this same field. A borrow of a different field is not. */ + if (!fe_own_access_whole_check(diags, state, access, loc)) return 0; + f = fe_own_field_slot(state, field, + access == FE_OWN_BORROW_SHARED || + access == FE_OWN_BORROW_MUT); + if (!f) { + /* No room left in the table, so this borrow covers the whole value. + That reports more than it has to and never less. */ + if (access == FE_OWN_BORROW_SHARED || access == FE_OWN_BORROW_MUT) + return fe_own_access_whole(diags, state, access, loc); + return 1; + } + switch (access) { + case FE_OWN_READ: + if (f->exclusive) { + fe_own_error_note(diags, loc, + "cannot read directly while value is mutably borrowed", + f->loc, "mutable borrow originated here"); + return 0; + } + return 1; + case FE_OWN_WRITE: + case FE_OWN_MOVE: + if (f->shared || f->exclusive) { + fe_own_error_note(diags, loc, + access == FE_OWN_WRITE ? "cannot write while value is borrowed" + : "cannot move while value is borrowed", + f->loc, "borrow originated here"); + return 0; + } + return 1; + case FE_OWN_BORROW_SHARED: + if (f->exclusive) { + fe_own_error_note(diags, loc, + "cannot create shared borrow while mutable borrow is live", + f->loc, "mutable borrow originated here"); + return 0; + } + if (!f->shared) f->loc = loc; + ++f->shared; + return 1; + case FE_OWN_BORROW_MUT: + if (f->shared || f->exclusive) { + fe_own_error_note(diags, loc, + "cannot create mutable borrow while another borrow is live", + f->loc, "existing borrow originated here"); + return 0; + } + f->exclusive = 1; + f->loc = loc; + return 1; + default: + break; + } + return 1; +} + +static int fe_own_access_whole_check(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) +{ + if (!fe_own_require_stable_borrow(diags, state, loc)) return 0; + if (access == FE_OWN_WRITE) { + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, "cannot write while value is borrowed", + state->borrow_loc, "borrow originated here"); + return 0; + } + return 1; + } + if (!fe_own_require_value(diags, state, loc)) return 0; + if (access == FE_OWN_READ || access == FE_OWN_BORROW_SHARED) { + if (state->exclusive) { + fe_own_error_note(diags, loc, access == FE_OWN_READ ? + "cannot read directly while value is mutably borrowed" : + "cannot create shared borrow while mutable borrow is live", + state->borrow_loc, "mutable borrow originated here"); + return 0; + } + return 1; + } + if (state->shared || state->exclusive) { + fe_own_error_note(diags, loc, access == FE_OWN_MOVE ? + "cannot move while value is borrowed" : + "cannot create mutable borrow while another borrow is live", + state->borrow_loc, "existing borrow originated here"); + return 0; + } + return 1; +} + +static int fe_own_access_whole(FeDiags *diags, FeOwnState *state, + FeOwnAccessKind access, FeLoc loc) { if (!state) return 0; if (access == FE_OWN_PROJECTION) return 1; @@ -225,9 +411,38 @@ void fe_own_release_exclusive(FeOwnState *state) if (!state->shared) state->borrow_loc = fe_own_no_loc(); } +/* Merging two paths through the code: a borrow that is live on either side is + live after, because the checker cannot know which side ran. */ +static void fe_own_merge_fields(FeOwnState *out, const FeOwnState *left, + const FeOwnState *right) +{ + unsigned i; + unsigned j; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) out->fields[i] = left->fields[i]; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *r = &right->fields[i]; + if (!r->name) continue; + for (j = 0; j < FE_OWN_FIELD_MAX; ++j) { + if (out->fields[j].name && + strcmp(out->fields[j].name, r->name) != 0) continue; + if (!out->fields[j].name) out->fields[j] = *r; + else { + if (r->shared > out->fields[j].shared) + out->fields[j].shared = r->shared; + if (r->exclusive && !out->fields[j].exclusive) { + out->fields[j].exclusive = 1; + out->fields[j].loc = r->loc; + } + } + break; + } + } +} + FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right) { FeOwnState out; + fe_own_merge_fields(&out, &left, &right); out.move = left.move == right.move ? left.move : fe_own_merge_move(left.move, right.move); out.initialized = left.initialized && right.initialized; @@ -246,6 +461,17 @@ FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right) int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right) { if (!left || !right) return 0; + { + unsigned i; + for (i = 0; i < FE_OWN_FIELD_MAX; ++i) { + const FeOwnField *a = &left->fields[i]; + const FeOwnField *b = &right->fields[i]; + if (!a->name != !b->name) return 0; + if (a->name && strcmp(a->name, b->name) != 0) return 0; + if (a->shared != b->shared || a->exclusive != b->exclusive) + return 0; + } + } return left->move == right->move && left->initialized == right->initialized && left->shared == right->shared && diff --git a/fec/src/own.h b/fec/src/own.h index 927ff97..a591b6d 100644 --- a/fec/src/own.h +++ b/fec/src/own.h @@ -34,6 +34,22 @@ typedef struct FeOwnPlace { int projected; } FeOwnPlace; +/* How many distinct fields of one value can be borrowed at once. Past this + a borrow falls back to covering the whole value, which reports more than it + has to but never less. */ +#define FE_OWN_FIELD_MAX 4 + +/* A borrow of one field rather than of the whole value. `self.bytes` and + `self.used_bytes` are different places, so borrowing one has to leave the + other readable -- otherwise a method cannot write through one field while + reading another, which is most of what a method does. */ +typedef struct FeOwnField { + const char *name; + unsigned shared; + int exclusive; + FeLoc loc; +} FeOwnField; + typedef struct FeOwnState { int move; int initialized; @@ -42,6 +58,10 @@ typedef struct FeOwnState { int borrow_conflict; FeLoc move_loc; FeLoc borrow_loc; + /* Whole-value state is above; these cover one field each. A whole-value + borrow conflicts with every field, and a field borrow conflicts with + the whole value and with itself. */ + FeOwnField fields[FE_OWN_FIELD_MAX]; } FeOwnState; typedef struct FeOwnProvenance { @@ -72,9 +92,15 @@ int fe_own_place_from_expr(FeNode *expr, FeOwnPlace *place); void fe_own_state_init(FeOwnState *state, int initialized); int fe_own_access(FeDiags *diags, FeOwnState *state, FeOwnAccessKind access, FeLoc loc); +/* The same, but reaching only one field of the value. A null `field` is the + whole value and behaves exactly as `fe_own_access`. */ +int fe_own_access_field(FeDiags *diags, FeOwnState *state, const char *field, + FeOwnAccessKind access, FeLoc loc); int fe_own_call_shared_view(FeDiags *diags, FeOwnState *state, FeLoc loc); void fe_own_release_shared(FeOwnState *state); void fe_own_release_exclusive(FeOwnState *state); +void fe_own_release_shared_field(FeOwnState *state, const char *field); +void fe_own_release_exclusive_field(FeOwnState *state, const char *field); FeOwnState fe_own_merge_state(FeOwnState left, FeOwnState right); int fe_own_state_equal(const FeOwnState *left, const FeOwnState *right); int fe_own_loop_merge_state(FeOwnState entry, FeOwnState backedge, diff --git a/fec/std/map.fe b/fec/std/map.fe index 3b50ec7..d598bb3 100644 --- a/fec/std/map.fe +++ b/fec/std/map.fe @@ -92,33 +92,22 @@ pub struct Map(V) { return; } - /// Make sure `need` bytes fit, moving to a bigger buffer if they do not. - /// - /// Kept apart from `keep` because the borrow that swaps the buffer in is - /// a borrow of the whole of `self` -- borrowing is tracked at the root -- - /// and it has to be over before any field is read again. - fn ensure_room(self: &mut Self, need: usize) -> !void { - let have: usize = self.bytes.^.n; - if need <= have { return; } - var room: usize = have; - while room < need { room = room * 2; } - let bigger: ^[]mut u8 = try mem.alloc_slice(u8, room); - var k: usize = 0; - let filled: usize = self.used_bytes; - while k < filled { - bigger.^[k] = self.bytes.^[k]; - k = k + 1; - } - let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); - mem.destroy(old); - return; - } - - /// Copy `key` into the byte buffer and point the slot at it. + /// Copy `key` into the byte buffer and point the slot at it, moving to a + /// bigger buffer first if it does not fit. fn keep(self: &mut Self, key: []u8, slot: usize) -> !void { let at: usize = self.used_bytes; - let need: usize = at + key.n; - try self.ensure_room(need); + if at + key.n > self.bytes.^.n { + var room: usize = self.bytes.^.n; + while room < at + key.n { room = room * 2; } + let bigger: ^[]mut u8 = try mem.alloc_slice(u8, room); + var k: usize = 0; + while k < at { + bigger.^[k] = self.bytes.^[k]; + k = k + 1; + } + let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); + mem.destroy(old); + } var i: usize = 0; while i < key.n { self.bytes.^[at + i] = key[i]; diff --git a/fec/tests/exec/fieldbrw.fe b/fec/tests/exec/fieldbrw.fe new file mode 100644 index 0000000..c512a74 --- /dev/null +++ b/fec/tests/exec/fieldbrw.fe @@ -0,0 +1,81 @@ +// EXIT:0 +// OUTPUT:a 40 b 3 +// OUTPUT:used 5 room 8 +// OUTPUT:first 9 +// OUTPUT:balanced +unit fieldbrw; + +import std.io; +import std.sys; +import std.mem; + +// Borrowing is per field. `p.a` and `p.b` are different places, so lending one +// out has to leave the other readable -- otherwise a method cannot write +// through one field while reading another, which is most of what a method +// does. + +struct Pair { a: i32, b: i32, } + +fn scale(v: &mut i32, by: i32) -> void { + v.^ = v.^ * by; + return; +} + +struct Box { + bytes: ^[]mut u8, + used: usize, + room: usize, + + fn with_capacity(n: usize) -> !Self { + let room: ^[]mut u8 = try mem.alloc_slice(u8, n); + return Self{ bytes: room, used: 0, room: n }; + } + + /// Move to a bigger buffer, then go on reading the other fields. The + /// borrow that hands over the buffer covers `bytes` and nothing else. + fn grow(self: &mut Self, want: usize) -> !void { + let bigger: ^[]mut u8 = try mem.alloc_slice(u8, want); + var i: usize = 0; + while i < self.used { + bigger.^[i] = self.bytes.^[i]; + i = i + 1; + } + let old: ^[]mut u8 = mem.replace(&mut self.bytes, bigger); + mem.destroy(old); + self.room = want; + return; + } + + fn push(self: &mut Self, v: u8) -> !void { + if self.used == self.room { try self.grow(self.room * 2); } + self.bytes.^[self.used] = v; + self.used = self.used + 1; + return; + } +} + +fn run() -> !void { + var p: Pair = Pair{ a: 4, b: 2 }; + let left: &mut i32 = &mut p.a; + // Writing another field while `a` is lent out. + p.b = 3; + scale(left, 10); + @print("a {} b {}\n", p.a, p.b); + + var box: Box = try Box.with_capacity(4); + try box.push(9); + try box.push(8); + try box.push(7); + try box.push(6); + try box.push(5); + @print("used {} room {}\n", box.used, box.room); + @print("first {}\n", box.bytes.^[0]); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/own/badrall.fe b/fec/tests/own/badrall.fe new file mode 100644 index 0000000..630a374 --- /dev/null +++ b/fec/tests/own/badrall.fe @@ -0,0 +1,14 @@ +// ERROR:11:borrow +unit badrall; + +struct Pair { a: i32, b: i32, } + +fn take(q: Pair) -> i32 { return q.a; } + +fn bad() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + let n = take(p); + left.^ = 4; + return n; +} diff --git a/fec/tests/own/badrfld.fe b/fec/tests/own/badrfld.fe index 65cb8b0..90a16e3 100644 --- a/fec/tests/own/badrfld.fe +++ b/fec/tests/own/badrfld.fe @@ -6,6 +6,6 @@ struct Pair { a: i32, b: i32, } fn bad() -> void { var p = Pair{ a: 1, b: 2 }; let left = &mut p.a; - p.b = 3; + p.a = 3; left.^ = 4; } diff --git a/fec/tests/own/badrsame.fe b/fec/tests/own/badrsame.fe new file mode 100644 index 0000000..b2e6342 --- /dev/null +++ b/fec/tests/own/badrsame.fe @@ -0,0 +1,12 @@ +// ERROR:9:borrow +unit badrsame; + +struct Pair { a: i32, b: i32, } + +fn bad() -> void { + var p = Pair{ a: 1, b: 2 }; + let one = &mut p.a; + let two = &mut p.a; + one.^ = 1; + two.^ = 2; +} diff --git a/fec/tests/own/okrfld.fe b/fec/tests/own/okrfld.fe new file mode 100644 index 0000000..5d68c28 --- /dev/null +++ b/fec/tests/own/okrfld.fe @@ -0,0 +1,14 @@ +unit okrfld; + +// Borrowing one field leaves the others alone. `p.a` and `p.b` are different +// places, so a borrow of one says nothing about the other. + +struct Pair { a: i32, b: i32, } + +fn ok() -> i32 { + var p = Pair{ a: 1, b: 2 }; + let left = &mut p.a; + p.b = 3; + left.^ = 4; + return p.a + p.b; +} From 51f555a830458c6e7c8bff142dbd1c226a72fd22 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 15:50:58 +0900 Subject: [PATCH 168/184] =?UTF-8?q?=EB=A6=AC=EC=A1=B8=EB=B2=84=EB=A5=BC=20?= =?UTF-8?q?=EC=9C=84=ED=95=9C=20=EC=9E=90=EB=A6=AC:=20Node.bind=20?= =?UTF-8?q?=EC=99=80=20Map.clear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ast.Node 가 이름이 무엇으로 해석됐는지 들고, Map 이 저장소를 유지한 채 키만 잊는다. 스코프가 끝날 때 표를 다음 스코프에 넘기는 것이 리졸버가 원하는 모양이다 -- 함수마다가 아니라 중첩 단계마다 표 하나. bind 는 Name 노드의 남는 a 필드를 재활용할 수도 있었지만 명시적인 쪽을 골랐다. 노드가 32 에서 36 바이트가 되는 값으로 그 자리가 무엇인지 이름이 말한다. clear 는 아무도 부르지 않는 채로 들어와 있었다. maps.fe 가 이제 부른다: 키가 사라지고, 방은 64 로 남고, 그 위에 다시 채워도 버퍼를 새로 잡지 않는다. cleared 0 room 64 gone 0 / refilled 3 / balanced GOAL.md 를 더했다. 외부 감사와, 그 항목들을 실제로 빌드해서 확인한 결과를 합친 P0~P4 다. 228/228, 32/32. --- AGENTS.md | 1 + GOAL.md | 100 ++++++++++++++++++++++++++++++++++ TODO.md | 2 + fec/std/map.fe | 14 +++++ fec/tests/exec/lexer/ast.fe | 4 ++ fec/tests/exec/lexer/parse.fe | 2 + fec/tests/exec/maps.fe | 22 ++++++++ 7 files changed, 145 insertions(+) create mode 100644 GOAL.md diff --git a/AGENTS.md b/AGENTS.md index 103579c..bc8dcf9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,7 @@ DOS/Windows용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. | `SPEC.md` | 언어 명세. 유일한 규범 문서 | | `IR.md` | 중간 표현. 프론트엔드와 기계 사이 | | `TODO.md` | 남은 작업, 미결 결정, 정해진 것 | +| `GOAL.md` | 부트스트랩까지의 실행 계획. P0~P4 와 안 하기로 한 것 | | `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 | ## 파이프라인 diff --git a/GOAL.md b/GOAL.md new file mode 100644 index 0000000..c2cfe0b --- /dev/null +++ b/GOAL.md @@ -0,0 +1,100 @@ +# GOAL — 부트스트랩까지 남은 작업 + +외부 스펙 감사(v0.1.8)와, 그 항목들을 실제로 빌드해서 확인한 결과를 합친 실행 +계획이다. 감사는 문서를 읽었고 여기 적힌 것은 컴파일러에 물어본 답이다. + +``` +uv run python tests/run.py 228/228 +uv run python tests/exec.py 32/32 +``` + +**검증 게이트: 매 항목마다 두 스위트 모두.** P0-1 과 P1-1 은 `run.py` 만으로는 +잡히지 않는다 -- 조용히 틀린 값을 내는 종류라 `exec.py` fixture 로 동작을 +고정해야 한다. + +**순서 근거:** P0 가 P3 를 막는다 (stdlib 표면 전체가 `?&T` 위에 얹힌다). +P1 은 P0-1 을 뺀 나머지 전부의 기준이다. P2 는 독립이라 언제 해도 된다. + +--- + +## P0 — 조용히 틀린 것 + +| # | 항목 | 무엇 | 왜 지금 | 규모 | +|---|---|---|---|---| +| 0-1 | **`?&T` lowering** | 옵셔널 참조를 니치로 표현한다 (참조는 null 이 될 수 없으므로 포인터 0 이 none). `== null`, `.?`, `if let`, `orelse` 전부 | `if let Some(r)` 이 **컴파일되고 쓰레기 값을 반환한다**. `== null` 은 `internal: cannot lower an aggregate as a value`. SPEC §5 R8 이 명시적으로 허용하는 타입인데 백엔드가 없다 | 중 | +| 0-2 | **제네릭 인스턴스 리터럴** | `Handle(Node){ raw: 7 }` 를 파싱한다 | 지금은 `Self{...}` 나 생성자 함수로만 인스턴스를 만들 수 있다. 타입 인자를 명시한 리터럴은 `expected ';'` 로 죽는다 | 소 | + +## P1 — 스펙의 빈칸 + +반나절짜리이고 이후 모든 결정의 기준이 된다. + +| # | 항목 | 결정 | 규모 | +|---|---|---|---| +| 1-1 | **정수 리터럴 기본 타입** | expected type 이 있으면 그 타입, 없으면 `i32`. 값이 대상 타입 범위를 벗어나면 컴파일 에러 (`let b: u8 = 300;`). `null` 이 이미 같은 방식이라 일관된다 | 중 | +| 1-2 | phantom 타입 파라미터 | §9 에 한 문장: "generic 타입 파라미터는 본문에서 사용되지 않아도 된다. `Handle(Node)` 와 `Handle(Type)` 은 서로 다른 nominal 인스턴스다." **구현은 이미 그렇게 동작한다** -- 미래의 구현자가 깨뜨리지 못하게 적어두는 것 | 소 | +| 1-3 | 오버플로 정의 | §7.4 에 "`--no-checks` 에서 오버플로 결과는 랩어라운드로 정의된다". i386 `add` 의 실제 동작이다 | 소 | +| 1-4 | 잔챙이 일곱 | 아래 표 | 중 | + +### 1-4 세부 + +| 항목 | 결정 | +|---|---| +| 정수·char `match` arm | `_` 필수 | +| enum payload 에 `^T`/drop 있는 타입 | 허용. drop 은 활성 배리언트만 | +| `for x in slice` 순회 중 원본 | 순회 동안 원본 root 는 대여 상태 | +| `defer` 안에서 `return` | 컴파일 에러 | +| `undefined` 배열 | 슬라이스로 넘겨 채우는 것만 허용 | +| 외부 유닛에서 private 필드가 있는 struct 리터럴 | 컴파일 에러. 생성자 함수 강제 | +| by-value `self` 에서 부분 이동 | `mem.replace` 필요. 예외는 자기 `drop` 안뿐 (§5 R7) | + +## P2 — SPEC 에서 죽은 백엔드 제거 + +C 백엔드는 없다. 파이프라인은 `.fe → i386 asm → wasm → wlink → .exe` 다. +그런데 `SPEC.md` 에 그 흔적이 열 군데 남아 있어서, 감사가 문서를 충실히 읽고 +존재하지 않는 문제(C 의 부호 있는 오버플로 UB)를 보고했다. + +| # | 항목 | 무엇 | 규모 | +|---|---|---|---| +| 2-1 | C 백엔드 잔재 | `.fei`, `--emit-c`, `fe_errors.h`, "호스트 C 방출", "C 방출 시 static inline" 열 군데 | 소 | +| 2-2 | 오류 코드 절 재작성 | `.fei` 기반 증분 빌드 서술을 실제대로 -- 드라이버가 빌드 전체에서 모아 철자 순으로 1부터 | 소 | + +## P3 — stdlib + +| # | 항목 | 표면 | 규모 | +|---|---|---|---| +| 3-1 | `Handle(T)` | **8바이트 고정.** `index 32 / slot_gen 16 / epoch 8 / arena_id 8`. `--no-checks` 는 **비교만 생략하고 레이아웃은 그대로** 둔다. 슬롯 gen 이 넘치면 그 슬롯은 영구 폐기 -- 랩어라운드로 stale 핸들이 되살아나는 것을 막는다 | 소 | +| 3-2 | `Arena(T)` | **짧은 대여만**: `alloc`, `get_copy`, `set`, `take`, `swap`, `free`, `reset`, `len`, `drop`. `reset` 은 슬롯별 gen 이 아니라 epoch 를 올린다 | 중 | +| 3-3 | `List(T)` 보강 | `pop`, `take`, `swap`, `slice`, `slice_mut` | 소 | +| 3-4 | `StringInterner` | `StrId{ raw: u32 }`. `intern`, `eq`, `eq_ids`, `hash`, `len`, `write`. **`str` 을 꺼내는 API 는 두지 않는다** -- 꺼내면 그 문자열이 사는 동안 interner 전체가 잠긴다 | 중 | +| 3-5 | `std.map` 판정 | 이미 `[]u8` 키라 별도 `IntMap` 이 필요 없다. `StrId` 를 4바이트 키로 쓰는지 확인만 | 소 | + +## P4 — 측정 + +| # | 항목 | 무엇 | 규모 | +|---|---|---|---| +| 4-1 | `--report-unsafe` | 유닛별 `unsafe` 블록 수, `*T` 출현 수, `*_unchecked` 호출 수. 목표는 `std.mem`/`std.sys` 밖 0 개. CI 에서 회귀 검사 -- 늘어나면 실패 | 소 | +| 4-2 | `--report-instances` | 제네릭 인스턴스 수와 추정 크기 | 소 | + +--- + +## 채택하지 않는 것 + +| 감사 항목 | 판정 | 근거 | +|---|---|---| +| 참조 튜플 `-> (&mut T, &mut T)` | **보류** | struct 는 필드 단위 대여로 이미 풀렸다 (`swap2(&mut p.a, &mut p.b)` 동작 확인). 컨테이너의 두 원소만 남는데 렉서·파서를 쓰면서 필요했던 자리가 0 번이다 | +| `Arena.get_mut -> ?&mut T` | **거부** | 감사 자신의 "참조를 오래 들고 있지 마라" 와 모순이다. 한 arena 안에서는 여전히 전체가 잠긴다 | +| `fmt.fmt_strid` | **거부** | `@print` 는 컴파일 단계에서 타입으로 `fmt_*` 를 고르는데, `StrId` 를 찍으려면 interner **인스턴스** 가 필요하고 R10 이 가변 전역 대여를 금지한다. `@print("{}", interner.text(id))` 로 간다 | +| 별도 `IntMap(V)` | **불필요** | `std.map` 이 `[]u8` 키라 이미 포괄한다 | +| C 오버플로 방출 규칙 | **대체** | C 백엔드가 없다. 결론(랩어라운드 정의)만 1-3 으로 흡수 | +| R4 완화 | **영구 제외** | 이걸 풀면 언어의 존재 이유가 없어진다 | + +## 이미 끝난 것 + +| 감사 항목 | 상태 | +|---|---| +| depth-1 field-sensitive 대여 | `f7e6676`. `std.map` 의 `keep` 이 다시 함수 하나가 됐다 | +| Copy AST 노드 | `ast.Node` 는 정수와 핸들뿐이라 자연히 Copy | +| 같은 struct 의 두 `&mut` | 필드 단위 대여로 풀림 | +| phantom 파라미터 동작 | 구현은 이미 지원. 문장만 P1-2 | +| Ferro 렉서·파서를 Ferro 로 | `fec/tests/exec/lexer/` | +| R4, R10, R9, 블록 표현식 배제, 오류 번호표, Copy handle enum | 손대지 않는다 | diff --git a/TODO.md b/TODO.md index 99bdcea..e5142df 100644 --- a/TODO.md +++ b/TODO.md @@ -34,6 +34,8 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 ## 셀프호스팅으로 가는 길 +순서와 근거는 `GOAL.md` 에 있다 (P0~P4). 여기는 무엇이 남았는지만 적는다. + | # | 일 | 규모 | 비고 | |---|---|---|---| | 1 | 심볼 표와 이름 해석을 Ferro 로 | 중 | `std.map` 이 준비됐다 | diff --git a/fec/std/map.fe b/fec/std/map.fe index d598bb3..58f4900 100644 --- a/fec/std/map.fe +++ b/fec/std/map.fe @@ -41,6 +41,20 @@ pub struct Map(V) { pub fn count_of(self: &Self) -> usize { return self.count; } + /// Forget every key but keep the storage. A scope that ends can hand its + /// table to the next one without going back to the allocator, which is + /// what a resolver wants: one table per nesting level, not per function. + pub fn clear(self: &mut Self) -> void { + var i: usize = 0; + while i < self.slots.^.n { + self.slots.^[i].used = false; + i = i + 1; + } + self.used_bytes = 0; + self.count = 0; + return; + } + pub fn room(self: &Self) -> usize { return self.slots.^.n; } /// Where `key` sits in the table: the slot holding it, or the first free diff --git a/fec/tests/exec/lexer/ast.fe b/fec/tests/exec/lexer/ast.fe index 9219e43..e27c56c 100644 --- a/fec/tests/exec/lexer/ast.fe +++ b/fec/tests/exec/lexer/ast.fe @@ -31,6 +31,10 @@ pub struct Node { pub a: usize, // handles into the same tree pub b: usize, pub next: usize, // the following statement, when there is one + /// What a `Name` resolved to: the handle of the `Let` or `Fn` that + /// declared it. The resolver writes this back so the tree carries its own + /// answers and nothing has to look the name up a second time. + pub bind: usize, } pub fn name_of(s: Shape) -> []u8 { diff --git a/fec/tests/exec/lexer/parse.fe b/fec/tests/exec/lexer/parse.fe index 50f759d..e991c51 100644 --- a/fec/tests/exec/lexer/parse.fe +++ b/fec/tests/exec/lexer/parse.fe @@ -66,6 +66,8 @@ pub struct Parser { let n: ast.Node = ast.Node{ shape: s, from: t.from, len: t.len, line: t.line, a: a, b: b, next: ast.NONE, + // The parser does not resolve names; the resolver writes this. + bind: ast.NONE, }; let i: usize = self.nodes.count(); try self.nodes.push(n); diff --git a/fec/tests/exec/maps.fe b/fec/tests/exec/maps.fe index c499f61..8c0d8a4 100644 --- a/fec/tests/exec/maps.fe +++ b/fec/tests/exec/maps.fe @@ -3,6 +3,8 @@ // OUTPUT:fn 2 let 3 missing 0 // OUTPUT:grown 64 // OUTPUT:after 40 +// OUTPUT:cleared 0 room 64 gone 0 +// OUTPUT:refilled 3 // OUTPUT:balanced unit maps; import std.io; @@ -37,6 +39,26 @@ fn run() -> !void { i = i + 1; } @print("after {}\n", found); + + // `clear` forgets every key and keeps the storage, which is what a scope + // that ends wants: hand the table to the next one without going back to + // the allocator. The room has to survive and the keys have to not. + let room: usize = seen.room(); + seen.clear(); + var gone: usize = 0; + i = 0; + while i < 40 { + let n: usize = fmt.fmt_i32(buf[..], i as i32); + if seen.has(buf[0..n]) { gone = gone + 1; } + i = i + 1; + } + @print("cleared {} room {} gone {}\n", seen.count_of(), room, gone); + + // And it is usable again afterwards, reusing the same buffers. + try seen.put("a", 1); + try seen.put("b", 2); + try seen.put("c", 3); + @print("refilled {}\n", seen.count_of()); return; } From dacf1e1b1babf79c0a3f404be14064a225d7f93a Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 15:56:48 +0900 Subject: [PATCH 169/184] =?UTF-8?q?GOAL=20P0-1:=20=EB=8B=88=EC=B9=98=20?= =?UTF-8?q?=EC=98=B5=EC=85=94=EB=84=90=EC=9D=B4=20=ED=8F=AC=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=20=EB=8C=80=EC=8B=A0=20=ED=8F=AC=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=EA=B0=80=20=EB=93=A0=20=EC=9E=90=EB=A6=AC=EB=A5=BC=20=EB=84=98?= =?UTF-8?q?=EA=B2=BC=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ?T 의 페이로드가 null 이 될 수 없으면 태그를 따로 두지 않고 그 불가능한 값을 null 로 쓴다. ?^T 와 ?&T 가 그렇다. 그런데 if let 이 그것을 풀 때 두 경우를 한 갈래로 처리하고 있었다. 바인딩이 참조인 이유가 둘이다. 페이로드가 값이면 바인딩은 그것이 래퍼 안에 앉은 자리를 가리켜야 하고(주소), 페이로드가 이미 포인터면 바인딩은 그 포인터여야 한다(값). 후자에 주소를 쓰면 포인터의 포인터가 되고, 프로그램은 값이 있어야 할 자리에서 주소를 읽는다. 컴파일도 되고 실행도 됐다. ?i32 5 (맞았음 -- 태그가 있어서 다른 길로 갔다) ?^i32 6125480 → 5 ?&i32 6125496 → 5 그리고 옵셔널을 null 과 비교하는 것이 lowering 되지 않았다 -- 래퍼 전체를 값으로 읽으려 해서 'cannot lower an aggregate as a value' 였다. 태그만 보면 되는 질문이다. optional/oknull.fe 가 검사만 하는 fixture 라 드러나지 않았다. exec/optref.fe 가 세 모양을 전부 고정한다: if let, orelse, .?, == null, 그리고 R7 관용구인 mem.replace(&mut box, null).? 로 소유자를 꺼내 놓는 것까지. 229/229, 33/33. --- fec/src/lowerexp.c | 22 ++++++++++ fec/src/lowerstm.c | 18 ++++++-- fec/tests/exec/optref.fe | 93 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 fec/tests/exec/optref.fe diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c index 2b97662..e457fa7 100644 --- a/fec/src/lowerexp.c +++ b/fec/src/lowerexp.c @@ -78,6 +78,28 @@ Slot lower_expr_core(Lower *L, FeNode *n) if (n->text && (!strcmp(n->text, "and") || !strcmp(n->text, "or"))) return lower_logical(L, n, !strcmp(n->text, "and")); op = binary_op(n->text, &is_cmp); + /* Comparing an optional with `null` asks about its tag, not about the + bytes of the whole wrapper -- which has no value form at all. */ + if ((op == FE_IR_EQ || op == FE_IR_NE) && n->a && n->b) { + FeNode *w = fe_m7_is_null(n->b) ? n->a : + (fe_m7_is_null(n->a) ? n->b : 0); + FeType *wt = w ? w->sem_type : 0; + if (wt && wt->kind == FE_TYPE_OPTIONAL) { + Slot s = lower_expr(L, w); + unsigned t0; + unsigned z; + if (!s.is_place) { + fail(L, "an optional with no place", w); + return slot_void(); + } + t0 = wrapper_tag(L, s, wt, w); + z = fe_ir_const(L->m, L->b, + uses_niche(wt) ? FE_IR_PTR : FE_IR_I8, 0); + return slot_value(fe_ir_binary(L->m, L->b, op, + uses_niche(wt) ? FE_IR_PTR : FE_IR_I8, t0, z, 1), + FE_IR_I8); + } + } operand = ir_type(n->a ? n->a->sem_type : 0); if (operand == FE_IR_VOID || operand == FE_IR_MEM) operand = FE_IR_I32; a = as_value(L, lower_expr(L, n->a), n->a); diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index cfc5f5d..33665e3 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -344,10 +344,20 @@ void lower_if_let(Lower *L, FeNode *n) FeType *bt = binding->sem_type; Slot payload = wrapper_payload(L, value, opt); unsigned local = declare_var(L, binding->cname, bt, binding->text); - if (bt && (bt->kind == FE_TYPE_REF || bt->kind == FE_TYPE_RAW)) - fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), - as_address(L, payload, n), FE_IR_PTR); - else + if (bt && (bt->kind == FE_TYPE_REF || bt->kind == FE_TYPE_RAW)) { + /* The binding is a reference either way, but for two different + reasons. When the payload is itself a single pointer (`^T`, + `&T`) the binding *is* that pointer, so it has to be read out. + When the payload is a value the binding points at where it sits + inside the wrapper, so the address is what is wanted. Taking the + address in the first case gives a pointer to the pointer, and + the program reads an address where it expects a value. */ + FeType *pl = opt ? (opt->kind == FE_TYPE_ERROR_UNION + ? opt->error_value : opt->elem) : 0; + unsigned p = pl && ir_type(pl) == FE_IR_PTR + ? as_value(L, payload, n) : as_address(L, payload, n); + fe_ir_store(L->m, L->b, fe_ir_at_local(local, 0), p, FE_IR_PTR); + } else store_into(L, fe_ir_at_local(local, 0), payload, n, ir_size(bt)); } lower_stmt(L, n->b); diff --git a/fec/tests/exec/optref.fe b/fec/tests/exec/optref.fe new file mode 100644 index 0000000..50725ae --- /dev/null +++ b/fec/tests/exec/optref.fe @@ -0,0 +1,93 @@ +// EXIT:0 +// OUTPUT:int 5 none 0 +// OUTPUT:own 5 absent -1 +// OUTPUT:ref 5 absent yes +// OUTPUT:cmp yes no +// OUTPUT:unwrap 5 +// OUTPUT:balanced +unit optref; + +import std.io; +import std.sys; +import std.mem; + +// An optional whose payload cannot be null keeps no separate tag: the payload's +// own impossible value is `null`. `?i32` carries a tag; `?^T` and `?&T` do not. +// +// Both shapes have to answer the same questions, and the pointer-shaped ones +// were answering with the address of the slot the pointer sits in rather than +// with the pointer -- one level of indirection too many, in code that compiled +// and ran. + +fn opt_int(on: bool) -> ?i32 { + if on { return 5; } + return null; +} + +/// A niche optional over an owner. The `if let` binding is `&i32` -- the +/// pointer itself, not where it is kept -- and `.?` moves the owner out so it +/// can be let go of. +fn take(o: ?^i32) -> i32 { + var box: ?^i32 = o; + var v: i32 = -1; + if let Some(p) = box { v = p.^; } + if box == null { return v; } + // SPEC 5 R7: a non-Copy value leaves a projection through `mem.replace`, + // which puts something valid back where it was. + let owner: ^i32 = mem.replace(&mut box, null).?; + mem.destroy(owner); + return v; +} + +fn make(v: i32) -> ?^i32 { + let p: ^i32 = mem.create(v) catch |e| { return null; }; + return p; +} + +struct Bag { + items: ^[]mut i32, + + /// SPEC 5 R8(a): derived from `self`, so the borrow is the caller's. + fn at(self: &Self, i: usize) -> ?&i32 { + if i >= self.items.^.n { return null; } + return &self.items.^[i]; + } +} + +fn run() -> !void { + // A tag in front of the payload. + var got: i32 = 0; + if let Some(a) = opt_int(true) { got = a; } + let none: i32 = opt_int(false) orelse 0; + @print("int {} none {}\n", got, none); + + // A niche over `^i32`. + @print("own {} absent {}\n", take(make(5)), take(null)); + + // A niche over `&i32`. + let room: ^[]mut i32 = try mem.alloc_slice(i32, 2); + room.^[0] = 5; + let b: Bag = Bag{ items: room }; + var seen: i32 = 0; + if let Some(r) = b.at(0) { seen = r.^; } + var past: bool = false; + if let Some(r) = b.at(9) { seen = seen; } else { past = true; } + @print("ref {} absent {}\n", seen, yesno(past)); + + // Comparing against null reads the tag, not the bytes of the wrapper. + @print("cmp {} {}\n", yesno(b.at(9) == null), yesno(b.at(0) == null)); + @print("unwrap {}\n", b.at(0).?.^); + return; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} From 32da64d7c131c6b0b5705830cc45b5263006b692 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 15:59:08 +0900 Subject: [PATCH 170/184] =?UTF-8?q?GOAL=20P0-2:=20Name(args){...}=20?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=9C=EB=84=A4=EB=A6=AD=20=EC=9D=B8=EC=8A=A4?= =?UTF-8?q?=ED=84=B4=EC=8A=A4=EB=A5=BC=20=EC=A7=93=EB=8A=94=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Name{...} 과 binding.Name{...} 만 있고 호출 뒤의 { 를 아무도 받지 않았다. 그래서 타입 인자를 명시한 리터럴이 파싱되지 않았고, 제네릭의 인스턴스는 Self{...} 나 생성자 함수로만 만들 수 있었다. 파서는 호출 뒤의 { 를 struct 리터럴로 받고, 체커는 그것을 타입 표기와 똑같은 resolver 에 넘긴다 -- 같은 철자니 같은 답이어야 한다. { 의 애매함은 이미 Name{...} 에 있던 것과 같고 같은 guard 가 정리한다. hold.Cell(i32){ v: 7 } 다른 유닛의 제네릭 hold.Handle(Node){ raw: 3 } 본문에서 T 를 안 쓰는 것 Boxed(i32){ v: 9 } 이 유닛의 제네릭 미사용 타입 파라미터는 그대로 둔다. typed handle 이 바로 그 모양이고, Handle(Node) 와 Handle(Kind) 가 실제로 다른 타입이라는 것은 badphant 가 거부로 고정한다. 233/233, 34/34. --- fec/src/checkexp.c | 11 +++++++++ fec/src/parser.c | 17 +++++++++++++- fec/tests/exec/geninst/README.md | 8 +++++++ fec/tests/exec/geninst/hold.fe | 14 ++++++++++++ fec/tests/exec/geninst/main.fe | 38 ++++++++++++++++++++++++++++++++ fec/tests/generic/badphant.fe | 15 +++++++++++++ fec/tests/generic/okphant.fe | 16 ++++++++++++++ 7 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 fec/tests/exec/geninst/README.md create mode 100644 fec/tests/exec/geninst/hold.fe create mode 100644 fec/tests/exec/geninst/main.fe create mode 100644 fec/tests/generic/badphant.fe create mode 100644 fec/tests/generic/okphant.fe diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index 3554e31..1a32aa5 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -249,6 +249,17 @@ FeType *check_struct_init(FeCheckerState *s, FeNode *n) } else if (n->children) err(s->c,n->loc,"empty enum variant cannot have payload"); n->sem_type=et; return et; } + if (n->a && n->a->kind==FE_N_CALL) { + /* `Name(args){...}` -- the same spelling a type annotation uses, so + the same resolver answers it. */ + int ok=0; + t=type_from_expr(s,n->a,&ok); + if (!ok || !t || t->kind!=FE_TYPE_STRUCT) { + err(s->c,n->a->loc,"unknown struct type"); + return unknown(s->c); + } + return check_struct_fields(s,n,t); + } t=fe_type_intern(&s->c->types,n->text ? n->text : ""); if (!t || t->kind!=FE_TYPE_STRUCT) { err(s->c,n->loc,"unknown struct type"); return unknown(s->c); } return check_struct_fields(s,n,t); diff --git a/fec/src/parser.c b/fec/src/parser.c index 430b31e..888f5f6 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -121,7 +121,22 @@ static FeNode *postfix(FeParser *p) FeNode *n=primary(p); for(;;) { FeToken t=p->current; FeNode *m; - if(eat(p,FE_TOK_LPAREN)) { m=toknode(p,FE_N_CALL,t); m->a=n; while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(m,delimited_expr(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' after call"); n=m; } + if(eat(p,FE_TOK_LPAREN)) { m=toknode(p,FE_N_CALL,t); m->a=n; while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(m,delimited_expr(p));if(!eat(p,FE_TOK_COMMA))break;} want(p,FE_TOK_RPAREN,"expected ')' after call"); n=m; + /* `Name(args){...}` builds an instance of a generic struct. Without + this the arguments have nowhere to go and only `Self{...}` or a + constructor can name one. Same `{` ambiguity as `Name{...}` + above, and the same guard settles it. */ + if(is(p,FE_TOK_LBRACE) && !p->forbid_struct_literal) { + FeNode *s=toknode(p,FE_N_STRUCT_INIT,t); s->a=n; next(p); + while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)) { FeNode *f; + if(!is_name(p)){error(p,"expected field name");recover(p);break;} + f=toknode(p,FE_N_FIELD,p->current);next(p); + want(p,FE_TOK_COLON,"expected ':' after field"); + f->a=expr(p,0);fe_node_add(s,f); + if(!eat(p,FE_TOK_COMMA))break; + } + want(p,FE_TOK_RBRACE,"expected '}' in struct literal"); n=s; + } } else if(eat(p,FE_TOK_LBRACKET)) { m=toknode(p,FE_N_INDEX,t);m->a=n; if(is(p,FE_TOK_DOTDOT)) { m->b=0; m->flags|=FE_NODE_SLICE; } else m->b=delimited_expr(p); diff --git a/fec/tests/exec/geninst/README.md b/fec/tests/exec/geninst/README.md new file mode 100644 index 0000000..7bfdd80 --- /dev/null +++ b/fec/tests/exec/geninst/README.md @@ -0,0 +1,8 @@ +# 제네릭 인스턴스 리터럴 + +`Name(args){...}` 와 `binding.Name(args){...}` 로 제네릭의 인스턴스를 짓는다. +전에는 `Self{...}` 나 생성자 함수로만 만들 수 있었다. + +`Handle(T)` 는 `T` 를 본문에서 쓰지 않는다 -- typed handle 의 자연스러운 모양이고, +`Handle(Node)` 와 `Handle(Kind)` 를 갈라놓는 것 말고는 하는 일이 없다. 그 둘이 +실제로 다른 타입이라는 것은 `generic/badphant.fe` 가 거부로 고정한다. diff --git a/fec/tests/exec/geninst/hold.fe b/fec/tests/exec/geninst/hold.fe new file mode 100644 index 0000000..44646f8 --- /dev/null +++ b/fec/tests/exec/geninst/hold.fe @@ -0,0 +1,14 @@ +unit hold; + +// Fields are `pub` so another unit can write the literal directly. A generic +// with a `pub` field is the only way to reach `binding.Name(args){...}`. +pub struct Cell(T) { + pub v: T, +} + +// `T` is never used in the body. That is the natural shape of a typed handle: +// the parameter is there to keep `Handle(Node)` and `Handle(Type)` apart, not +// to describe any storage. +pub struct Handle(T) { + pub raw: u32, +} diff --git a/fec/tests/exec/geninst/main.fe b/fec/tests/exec/geninst/main.fe new file mode 100644 index 0000000..b469b2a --- /dev/null +++ b/fec/tests/exec/geninst/main.fe @@ -0,0 +1,38 @@ +// EXIT:0 +// OUTPUT:cell 7 41 +// OUTPUT:handle 3 4 sum 7 +// OUTPUT:local 9 +unit main; + +import std.io; +import hold; + +struct Node { v: i32, } +struct Kind { v: i32, } + +// A generic declared here, instantiated with an explicit argument below. +struct Boxed(T) { + v: T, +} + +// Two instances of the same phantom generic are different nominal types, so +// this only accepts one of them. +fn only_node(h: hold.Handle(Node)) -> u32 { return h.raw; } +fn only_kind(h: hold.Handle(Kind)) -> u32 { return h.raw; } + +fn main() -> i32 { + // `binding.Name(args){...}` -- a generic instance from another unit. + let a: hold.Cell(i32) = hold.Cell(i32){ v: 7 }; + let b: hold.Cell(u8) = hold.Cell(u8){ v: 41 }; + @print("cell {} {}\n", a.v, b.v); + + let n: hold.Handle(Node) = hold.Handle(Node){ raw: 3 }; + let k: hold.Handle(Kind) = hold.Handle(Kind){ raw: 4 }; + @print("handle {} {} sum {}\n", only_node(n), only_kind(k), + only_node(n) + only_kind(k)); + + // `Name(args){...}` -- a generic declared in this unit. + let c: Boxed(i32) = Boxed(i32){ v: 9 }; + @print("local {}\n", c.v); + return 0; +} diff --git a/fec/tests/generic/badphant.fe b/fec/tests/generic/badphant.fe new file mode 100644 index 0000000..6735b73 --- /dev/null +++ b/fec/tests/generic/badphant.fe @@ -0,0 +1,15 @@ +// ERROR:14:argument type mismatch +unit badphant; + +// A type parameter that the body never mentions still tells two instances +// apart. `Handle(Node)` and `Handle(Kind)` are different nominal types. +struct Handle(T) { raw: u32, } + +struct Node { v: i32, } +struct Kind { v: i32, } + +fn only_node(h: Handle(Node)) -> u32 { return h.raw; } + +fn bad() -> u32 { + return only_node(Handle(Kind){ raw: 1 }); +} diff --git a/fec/tests/generic/okphant.fe b/fec/tests/generic/okphant.fe new file mode 100644 index 0000000..dad55a0 --- /dev/null +++ b/fec/tests/generic/okphant.fe @@ -0,0 +1,16 @@ +unit okphant; + +// The parameter is used nowhere in the body. That is legal, and it is the +// natural shape of a typed handle. +struct Handle(T) { raw: u32, } + +struct Node { v: i32, } +struct Kind { v: i32, } + +fn only_node(h: Handle(Node)) -> u32 { return h.raw; } +fn only_kind(h: Handle(Kind)) -> u32 { return h.raw; } + +fn ok() -> u32 { + return only_node(Handle(Node){ raw: 1 }) + + only_kind(Handle(Kind){ raw: 2 }); +} From 1a368dc13fa86d6a3b889f024785738c2b2bd328 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:07:45 +0900 Subject: [PATCH 171/184] =?UTF-8?q?GOAL=20P1-1..1-3:=20=EB=A6=AC=ED=84=B0?= =?UTF-8?q?=EB=9F=B4=EC=9D=B4=20=EC=9E=90=EA=B8=B0=20=ED=83=80=EC=9E=85?= =?UTF-8?q?=EC=97=90=20=EC=95=88=20=EB=A7=9E=EC=9C=BC=EB=A9=B4=20=EA=B1=B0?= =?UTF-8?q?=EB=B6=80=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 그리고 그 자리를 파다가 더 나쁜 것이 나왔다. store 의 폭이 목적지가 아니라 값에서 왔다. 정수 리터럴은 더 좁은 것이 요구하기 전까지 i32 이므로 let b: u8 = 200; 은 4바이트가 1바이트 자리로 가는 것으로 도착하고, 4바이트를 쓰면 프레임이 그 옆에 놓은 것을 지운다. let a: i32 = 5; let b: u8 = 300; let d: u8 = 44; a 0 / b 0 / d 44 → a 5 / b 44 / d 44 폭 넓은 지역 하나만 있으면 드러나지 않아서 여태 살아 있었다. exec/narrow.fe 가 폭이 섞인 지역을 나란히 두어 고정한다. 규칙 자체는 SPEC §3 에 넣었다: 리터럴의 타입은 문맥이 요구하는 정수 타입이고, 없으면 i32 다. 범위를 벗어나면 잘리는 것이 아니라 거부된다. 앞의 단항 - 는 리터럴의 일부로 보아 i8 = -128 은 되고 u8 = -1 은 안 된다. 같이 넣은 문장 둘: - §9 미사용 타입 파라미터는 정상이다. typed handle 이 그 모양이고 구현은 이미 그렇게 동작했다. - §7.4 --no-checks 에서 오버플로는 랩어라운드로 정의된다. 타깃이 실제로 하는 일이고 미정의로 두지 않는다. 237/237, 35/35. --- SPEC.md | 9 +- fec/src/checkstm.c | 71 +++++++++++ fec/src/lowerstm.c | 8 +- fec/tests/exec/narrow.fe | 32 +++++ fec/tests/types/badlit.fe | 10 ++ fec/tests/types/badlitng.fe | 8 ++ fec/tests/types/oklit.fe | 17 +++ handoff2.md | 244 ------------------------------------ 8 files changed, 152 insertions(+), 247 deletions(-) create mode 100644 fec/tests/exec/narrow.fe create mode 100644 fec/tests/types/badlit.fe create mode 100644 fec/tests/types/badlitng.fe create mode 100644 fec/tests/types/oklit.fe delete mode 100644 handoff2.md diff --git a/SPEC.md b/SPEC.md index 150c909..c0982be 100644 --- a/SPEC.md +++ b/SPEC.md @@ -54,7 +54,7 @@ DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성 - 식별자: `[A-Za-z_][A-Za-z0-9_]*`. 대소문자 구분. - 주석: `//` 줄 끝까지, `/* */` **중첩 허용**. -- 정수 리터럴: `123`, `0xFF`, `0b1010`, `0o17`, 자릿수 구분 `1_000_000`. +- 정수 리터럴: `123`, `0xFF`, `0b1010`, `0o17`, 자릿수 구분 `1_000_000`. **타입은 문맥이 요구하는 정수 타입이고, 요구하는 것이 없으면 `i32`다.** 값이 그 타입의 범위를 벗어나면 컴파일 에러다 — `let b: u8 = 300;`은 44로 잘리는 것이 아니라 거부된다. 앞의 단항 `-`는 리터럴의 일부로 보아 `let n: i8 = -128;`은 허용하고 `let u: u8 = -1;`은 거부한다. - 문자 리터럴: `'a'`, `'\n'`, `'\x41'` → 타입 `char`. - 문자열 리터럴: `"abc"` → 타입 `str`. NUL 종료 아님. 이스케이프는 문자 리터럴과 동일. 인접 리터럴 자동 연결 없음. - 불린: `true`, `false`. 옵셔널 널: `null`. @@ -569,7 +569,7 @@ pub fn main() -> !void { 동작: `core.panic(msg: str, file: str, line: u32)` 호출 → 등록된 `sys.on_exit(fn)` 정리 함수를 역순 호출 → 메시지 출력 → `sys.exit(3)`. 사용자가 `core.set_panic_handler`로 교체 가능. 일반 panic unwind나 defer 실행은 없지만 프로세스 종료 전에 반드시 복원해야 하는 자원은 allocation 없는 고정 크기 `on_exit` registry에 등록한다. -`--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. +`--no-checks` 빌드에서 제거되는 것: 경계 검사, 오버플로 검사, `.?` 검사. **오버플로 검사가 없을 때 `+ - * /`의 결과는 랩어라운드로 정의된다** — 타깃의 정수 연산이 그대로 하는 일이며, 미정의 동작으로 두지 않는다. 즉 `--no-checks`에서 `a + b`는 `a +% b`와 같은 값을 낸다. **절대 제거되지 않는 것:** 소유권/참조 검사, 옵셔널 타입 검사, `match` 완전성 — 전부 컴파일타임이므로. ### 7.5 comptime @@ -658,6 +658,11 @@ identity를 만들지 않는다. `comptime` type 파라미터 기반 모노모피제이션. v0.1의 user-defined generic parameter는 `type`만 지원한다. +타입 파라미터는 본문에서 사용되지 않아도 된다. `struct Handle(T) { raw: u32 }`는 +정상이며 `Handle(Node)`와 `Handle(Kind)`는 서로 다른 nominal 인스턴스다 — 파라미터가 +하는 일이 저장소를 서술하는 것이 아니라 두 인스턴스를 갈라놓는 것뿐인 경우이고, +typed handle이 정확히 그 모양이다. 미사용 파라미터에 경고를 내지 않는다. + ```fe pub struct List(T) { items: ^[]T, diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 98f4d01..f8761ff 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -549,6 +549,62 @@ int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) return compatible(want,got,value); } +/* The magnitude an integer literal spells, ignoring any sign. The same shape + lowering uses on the same text, so the two cannot disagree about what was + written. */ +static unsigned long literal_magnitude(const char *s) +{ + unsigned long v = 0; + if (!s) return 0; + if (s[0]=='0' && (s[1]=='x' || s[1]=='X')) { + for (s += 2; *s; ++s) { + int d = *s>='0'&&*s<='9' ? *s-'0' : + *s>='a'&&*s<='f' ? *s-'a'+10 : + *s>='A'&&*s<='F' ? *s-'A'+10 : -1; + if (d < 0) { if (*s=='_') continue; break; } + v = v*16UL + (unsigned long)d; + } + return v; + } + for (; *s; ++s) { + if (*s=='_') continue; + if (*s<'0' || *s>'9') break; + v = v*10UL + (unsigned long)(*s-'0'); + } + return v; +} + +/* SPEC 4.1: an integer literal takes the type its context asks for, and a + value that does not fit that type is a mistake where it is written rather + than a truncation nobody sees. */ +static int literal_fits(const FeType *want, const char *text, int negative) +{ + unsigned long v; + unsigned long limit; + unsigned bits; + if (!want || want->kind != FE_TYPE_INT || !text) return 1; + bits = want->bits ? want->bits : 32U; + if (bits > 32U) bits = 32U; + v = literal_magnitude(text); + if (want->is_unsigned) { + if (negative) return v == 0UL; + if (bits >= 32U) return 1; + return v <= (1UL << bits) - 1UL; + } + limit = bits >= 32U ? 2147483647UL : (1UL << (bits - 1U)) - 1UL; + return v <= (negative ? limit + 1UL : limit); +} + +/* Is this node a plain integer literal, rather than a character, a string, or + one of the word-shaped literals? */ +static int plain_int_literal(const FeNode *n) +{ + return n && n->kind==FE_N_LITERAL && n->text && + n->text[0]!='\'' && n->text[0]!='"' && + strcmp(n->text,"true") && strcmp(n->text,"false") && + strcmp(n->text,"null") && strcmp(n->text,"undefined"); +} + FeType *m7_check_expected(FeCheckerState *s, FeNode *value, FeType *expected) { @@ -573,6 +629,21 @@ FeType *m7_check_expected(FeCheckerState *s, FeNode *value, value->sem_context=expected; return expected; } + /* An integer literal is `i32` on its own; where an integer type is asked + for it is that type instead, and it has to fit in it. */ + if (expected && expected->kind==FE_TYPE_INT) { + FeNode *lit = plain_int_literal(value) ? value : + (value->kind==FE_N_UNARY && value->text && + !strcmp(value->text,"-") && plain_int_literal(value->a) + ? value->a : 0); + if (lit) { + if (!literal_fits(expected, lit->text, lit!=value)) + err(s->c,value->loc,"integer literal out of range for its type"); + lit->sem_type=expected; + value->sem_type=expected; + return expected; + } + } actual=check_expr(s,value); if (!expected) return actual; if (expected->kind==FE_TYPE_OPTIONAL && expected->elem && diff --git a/fec/src/lowerstm.c b/fec/src/lowerstm.c index 33665e3..78862ac 100644 --- a/fec/src/lowerstm.c +++ b/fec/src/lowerstm.c @@ -8,7 +8,13 @@ void store_into(Lower *L, FeIrPlace dst, Slot value, FeNode *n, fe_ir_copy(L->m, L->b, dst, value.place, size); return; } - fe_ir_store(L->m, L->b, dst, as_value(L, value, n), value.type); + /* How wide the store is belongs to the place, not to the value. An + integer literal is `i32` until something narrower asks for it, so + `let b: u8 = 200;` arrives here as four bytes going into one -- and + writing four wipes out whatever the frame put next to it. */ + fe_ir_store(L->m, L->b, dst, as_value(L, value, n), + size == 1UL ? FE_IR_I8 : + size == 2UL ? FE_IR_I16 : value.type); } void lower_return(Lower *L, FeNode *n) diff --git a/fec/tests/exec/narrow.fe b/fec/tests/exec/narrow.fe new file mode 100644 index 0000000..bf7c30d --- /dev/null +++ b/fec/tests/exec/narrow.fe @@ -0,0 +1,32 @@ +// EXIT:0 +// OUTPUT:a 5 b 200 c 44 d 9 +// OUTPUT:e 5 f 1 g 65535 +// OUTPUT:sum 253 +unit narrow; + +import std.io; + +// How wide a store is belongs to the place, not to the value. An integer +// literal is `i32` until something narrower asks for it, so `let b: u8 = 200;` +// arrives at the store as four bytes going into one -- and writing four wipes +// out whatever the frame put beside it. +// +// Several locals of mixed width, next to each other, is what it takes to see +// it: each narrow store used to reach back over the one declared before it. + +fn main() -> i32 { + let a: i32 = 5; + let b: u8 = 200; + let c: u8 = 44; + let d: i16 = 9; + @print("a {} b {} c {} d {}\n", a, b, c, d); + + let e = 5; // no annotation: i32 (SPEC 4.1) + let f: i8 = 1; + let g: u16 = 65535; + @print("e {} f {} g {}\n", e, f as i32, g); + + // And the values are still there after everything else was written. + @print("sum {}\n", (b as i32) + (c as i32) + (d as i32) + e - (a as i32)); + return 0; +} diff --git a/fec/tests/types/badlit.fe b/fec/tests/types/badlit.fe new file mode 100644 index 0000000..ede7d0a --- /dev/null +++ b/fec/tests/types/badlit.fe @@ -0,0 +1,10 @@ +// ERROR:8:integer literal out of range +unit badlit; + +// SPEC 4.1: an integer literal takes the type its context asks for. 300 is not +// a u8, and truncating it silently is the one thing that must not happen. + +fn bad() -> u8 { + let n: u8 = 300; + return n; +} diff --git a/fec/tests/types/badlitng.fe b/fec/tests/types/badlitng.fe new file mode 100644 index 0000000..b222970 --- /dev/null +++ b/fec/tests/types/badlitng.fe @@ -0,0 +1,8 @@ +// ERROR:6:integer literal out of range +unit badlitng; + +// An unsigned type has no negative values to truncate to. +fn bad() -> u8 { + let n: u8 = -1; + return n; +} diff --git a/fec/tests/types/oklit.fe b/fec/tests/types/oklit.fe new file mode 100644 index 0000000..7394afb --- /dev/null +++ b/fec/tests/types/oklit.fe @@ -0,0 +1,17 @@ +unit oklit; + +// The edges of each type are in range, and a literal with no context is i32. + +fn ok() -> i32 { + let a: u8 = 255; + let b: i8 = -128; + let c: i8 = 127; + let d: u16 = 65535; + let e: i32 = 2147483647; + let f: i32 = -2147483648; + let g: u8 = 0xFF; + let h = 5; + let i: usize = 4294967295; + return h + (a as i32) + (b as i32) + (c as i32) + (d as i32) + + (e - e) + (f - f) + (g as i32) + (i as i32); +} diff --git a/handoff2.md b/handoff2.md deleted file mode 100644 index d051e48..0000000 --- a/handoff2.md +++ /dev/null @@ -1,244 +0,0 @@ -# Handoff 2 — 마커 증거 수집과 fixture 이름 짓기 - -앞선 시도가 한 번 실패해서 되돌렸다(`7636a41`). 실패 원인부터 읽어라. -그게 이 문서의 절반이다. - -## 실패한 방식 - -지난번 에이전트는 **파일을 열지 않고 이름 문자열만 변환했다.** 밑줄을 지우고, -그 결과 충돌하는 이름에 숫자를 붙였다. - -``` -bad_loop → badlop1 badweak → badweak2 -badloop → badlp2 oktry → oktry2 -``` - -결정적 증거는 이것이다. `own/badfld.fe` 는 **구조체 필드에 참조를 둔 것**을 검사하고 -`types/badfld.fe` 는 **없는 필드에 접근한 것**을 검사한다. 완전히 다른 규칙인데 -둘 다 `badfmem.fe` 가 됐다. 같은 옛 이름에 같은 치환을 먹였기 때문이다. - -**이 일의 본질은 파일을 읽고 무엇을 검사하는지 판단하는 것이다.** 이름 변환이 아니다. - ---- - -## 배경 - -`fec` 는 Ferro 언어의 컴파일러이고 현재 **프론트엔드만** 있다. fixture 는 컴파일러가 -어떤 코드를 받아들이고 어떤 코드를 거부하는지 고정하는 테스트다. 파일 하나가 케이스 -하나다. - -```powershell -uv run python tests/run.py # 전체 -uv run python tests/run.py -k own # 경로에 own 이 들어간 것만 -``` - -러너는 매번 `.build\fec.exe` 를 새로 빌드한다. 진단 전문을 보려면 그걸 직접 부른다. - -``` -> .build\fec.exe --check fec\tests\types\bad_ari.fe -fec/tests/types/bad_ari.fe:8:15: error: wrong number of arguments - 8 | return add(1); - | ^ -``` - -### 러너가 기대를 정하는 방법 — 이걸 정확히 알아야 한다 - -`tests/run.py` 의 `expectation()` 을 직접 읽어라. 요약하면: - -| 파일 첫 줄 | 러너의 기대 | -|---|---| -| `// ERROR:7:borrow` | **거부**되고, 진단이 **7번 줄**, 문구에 **`borrow`** 포함 | -| `// ERROR:borrow` | 거부되고 문구에 `borrow` 포함 (줄은 안 봄) | -| **마커 없음** | **파일명이 `bad` 로 시작하면 거부**, 아니면 **성공** | - -마지막 줄이 이 작업의 핵심 함정이다. - -> ### 마커가 없는 파일에서는 `bad` 접두사가 기대값 그 자체다 -> -> 마커 없는 `bad_ari.fe` 를 `arity.fe` 로 바꾸면 러너는 그 순간부터 -> **성공하기를** 기대한다. 그리고 그 fixture 는 실패한다. -> -> 마커가 **있는** 파일은 마커가 기대를 정하므로 접두사가 아무 의미도 없다. -> 마음대로 지어도 된다. - -그래서 이 핸드오프는 마커 없는 37개를 **먼저** 처리한다. - ---- - -## 착수 전 기준선 - -``` -uv run python tests/run.py -→ 150/188 passed (58 pin a line and message) -``` - -**두 숫자 모두 끝까지 변하면 안 된다.** - -- 줄면 무언가 깨진 것이다 -- **늘면 검사를 약화시킨 것이다.** 이쪽이 더 나쁘다. 조용히 통과하는 테스트는 - 없는 테스트보다 해롭다 - -어느 쪽이든 되돌리고 보고하라. - ---- - -# 1. 마커 없는 fixture 37개 — 증거만 모은다 - -이 37개는 마커가 없어서 **"거부되기만 하면 통과"** 다. 엉뚱한 이유로 거부돼도 초록이다. -마커를 붙여야 하는데 **그 판정은 네가 하지 않는다.** - -``` -types/ 19 bad_ari bad_asgn bad_cast bad_cond bad_mlet bad_ret bad_shwr - bad_type bad_unit bad_unk bad_void badarr badchar badcycle - badfield badfld badindex badmat badstr - -format/ 10 bad_ari bad_bufw bad_cls bad_many bad_open bad_run bad_try - bad_type bad_verb bad_writ - -own/ 8 bad_clos bad_cond bad_dbl bad_dest bad_drop bad_loop bad_move - bad_proj -``` - -## 왜 판정을 맡기지 않는가 - -판정 결과가 세 갈래로 갈리는데 그중 하나는 **컴파일러를 고쳐야 하는 경우**다. - -| 결론 | 조치 | -|---|---| -| 마커를 안 붙였을 뿐, 진단은 옳다 | 마커를 쓴다 | -| 진단은 나오지만 **다른 이유**로 거부하고 있다 | fixture 를 다시 본다 | -| **진단이 부실하다** — 규칙 위반을 못 짚고 뭉뚱그린 오류만 낸다 | **컴파일러를 고친다** | - -세 번째가 실제로 있었다. `own/badweak.fe` 는 mut 대여를 shared 로 약화시키는 것을 -검사하는데 컴파일러는 `type mismatch` 라고만 했다. 실제 출력을 마커에 그대로 -베꼈다면 초록이 되면서 **컴파일러의 부실한 진단이 정답으로 굳었을 것이다.** - -그래서 **너는 증거를 모으고 판정은 사람이 한다.** - -## 파일마다 보고할 것 - -``` -파일 fec/tests/types/bad_ari.fe -검사 대상 이 코드가 무엇을 위반하려 하는가 — 네가 읽고 판단한 것 -근거 그렇게 본 이유. 어느 줄의 무엇 때문인지 -실제 진단 .build\fec.exe --check <경로> 의 출력 전문 (줄·열·문구 그대로) -일치 여부 실제 진단이 '검사 대상' 을 짚는가 — 예 / 아니오 / 애매 -``` - -`일치 여부` 가 이 작업의 산출물이다. 나머지는 그 판단의 근거다. -**애매하면 애매하다고 써라.** 억지로 '예' 로 만들면 이 작업이 무의미해진다. - -## 절대 금지 - -- **`// ERROR:` 마커를 하나도 쓰지 마라.** 이 항목의 산출물은 보고서뿐이다 -- `fec/src/` 의 어떤 파일도 고치지 마라 -- `tests/run.py` 를 고치지 마라 (읽는 건 권장) -- fixture 의 내용을 고치지 마라 -- **실제 출력을 그대로 마커로 옮기는 것** — 가장 하기 쉽고 가장 해로운 실수다 - -## 산출물 - -리포지터리 루트에 `fixture-report.md`. 디렉터리별로 나누고 위 다섯 항목을 담는다. -이것만 커밋한다. - ---- - -# 2. fixture 이름 짓기 - -1번을 **끝내고 보고한 뒤에** 시작한다. 1번의 판정 결과가 이름을 바꾸기 때문이다. - -대상은 네 디렉터리의 모든 `.fe` 파일이다. - -``` -fec/tests/types/ 31 -fec/tests/format/ 13 -fec/tests/own/ 50 -fec/tests/optional/ 28 -``` - -`units/` `generic/` `parse/` `pending-backend/` 는 **제외한다.** -(`units/` 와 `generic/` 은 이름이 import 경로의 일부라 구조가 다르다.) - -이미 제대로 된 이름 셋은 손대지 마라. - -``` -own/globalm.fe own/localesc.fe own/self_fld.fe -``` - -## 이름 제약 (컴파일러가 강제한다) - -각 `.fe` 는 `unit <이름>;` 을 갖고 **그 이름이 파일명(확장자 제외)과 정확히 같아야 한다.** -이름은 소문자로 시작, `a-z0-9_` 만, **최대 8자**. DOS 8.3 에서 온 제약이고 -`SPEC.md` §8.1 의 일부라 바꿀 수 없다. - -### 디렉터리가 다르면 이름이 겹쳐도 된다 - -지금 `types/bad_ari.fe` 와 `format/bad_ari.fe` 가 **동시에 존재하고 테스트는 통과한다.** -각 fixture 는 독립된 빌드다. 지난번 실패는 이걸 몰라서 억지로 유일하게 만들려다 -숫자를 붙인 것이다. **각 디렉터리 안에서만 유일하면 된다.** - -### 접두사 - -| | | -|---|---| -| **마커가 있는 파일** | `bad`/`ok` 접두사를 **버려도 된다.** 기대는 마커가 정한다. 8자를 접두사에 쓰지 마라 | -| **1번의 37개** | 사람이 마커를 붙이기 전까지 **`bad` 접두사를 반드시 유지해야 한다.** 남는 건 5자다 | - -37개는 5자 안에 뜻을 담기 어려우니 **가능한 만큼만 개선하고, 안 되는 건 그대로 두고 -목록에 적어라.** 마커가 붙으면 그때 다시 짓는다. - -## 절차 - -파일 하나마다: - -1. **연다.** 전체를 읽는다. 대개 10줄 미만이다 -2. **무엇을 검사하는지 판단한다.** 마커가 있으면 강한 단서다 -3. 그것을 8자 안에 나타내는 이름을 짓는다 -4. `git mv` 로 옮긴다 -5. **파일 안 `unit` 선언을 새 이름으로 고친다.** 안 하면 컴파일러가 거부한다 -6. 그 외에는 파일을 **한 글자도** 건드리지 마라 - -## 이름의 기준 - -이름은 **"무엇을 검사하는가"** 를 나타낸다. - -``` -좋음: badgmut → globalm 전역을 mut 로 빌리는 것 - badlocsl → localesc 지역 변수의 참조가 탈출하는 것 - badfld → reffield 구조체 필드에 참조를 둔 것 (own/) - badfld → nofield 없는 필드에 접근한 것 (types/) - -나쁨: badarr → ba1 아무것도 말하지 않음 - badweak → badweak2 숫자는 정보가 아님 - badcatch → badcatc 그냥 자른 것 -``` - -축약은 해도 된다. 다만 **읽어서 짐작이 가야 한다.** - -## 절대 금지 - -- **첫 줄 `// ERROR:` 마커를 만들거나 고치거나 지우지 마라** -- `unit` 선언 외의 내용 변경 -- 빈 줄 추가 — 지난번에 119개 파일에 군더더기 빈 줄이 들어갔다 - -## 진행 방법 - -**디렉터리 하나씩** 끝내고 `-k <디렉터리>` 로 확인한 뒤 커밋해라. 범위가 좁아야 -문제를 찾는다. - ---- - -# 커밋 - -1번은 하나, 2번은 디렉터리마다 하나. 메시지는 무엇을 왜 바꿨는지 한국어로. -**푸시하지 마라.** - -# 최종 보고 - -- 각 커밋 전후의 두 숫자 (`N/188`, `M pin`) -- 1번: `fixture-report.md` 경로, 그리고 **`일치 여부: 아니오 / 애매`** 로 판정한 것의 목록. - 거기가 컴파일러를 고쳐야 할 수도 있는 지점이라 가장 중요하다 -- 2번: **바꾼 이름 전체 목록** — `디렉터리/이전 → 이후` 와 각각 **한 줄 근거**. - 근거가 안 써지는 이름은 잘못 지은 것이다 -- 2번에서 이름을 못 지은 파일 목록과 이유. 억지로 짓지 말고 남겨라 -- 판단이 필요해서 건너뛴 것 From d9bced830c88a5374142532781933c62a9451b9b Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:10:44 +0900 Subject: [PATCH 172/184] =?UTF-8?q?GOAL=20P1-4:=20=EC=9E=91=EC=9D=80=20?= =?UTF-8?q?=EA=B7=9C=EC=B9=99=20=EC=9D=BC=EA=B3=B1=EC=9D=84=20=EC=A0=95?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20=EC=96=B4=EA=B8=8B=EB=82=9C=20=EB=91=98?= =?UTF-8?q?=EC=9D=84=20=EA=B3=A0=EC=B9=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일곱 중 다섯은 구현이 이미 옳게 답하고 있었다 -- match 는 enum 전용이고, enum payload 에 소유 타입을 담을 수 있고, undefined 배열은 원소 단위로 추적하지 않고, 다른 유닛에서 private 필드가 있는 리터럴은 거부되고, by-value self 의 부분 이동도 mem.replace 가 필요하다. SPEC §7.9 에 적었다. 나머지 둘은 그냥 통과하고 있었다. defer { return 1; } 이 받아들여졌다. 지연 블록은 함수가 무엇을 반환할지 이미 정한 뒤 스코프 정리 중에 도는데 거기서 return 이 뜻할 것이 없다. for x in xs { xs[0] = 9; } 도 받아들여졌다. x 가 xs 안을 가리키는 참조이니 순회 몸통에서 xs 에 쓰는 것은 그 참조가 가리키는 것을 옮기는 일이다 (R6). 순회는 이제 도는 동안 대여한다. 읽기는 공유 순회에서 그대로 된다. 240/240, 35/35. --- SPEC.md | 14 ++++++++++++++ fec/src/checkcal.c | 5 +++++ fec/src/checkstm.c | 16 ++++++++++++++++ fec/tests/own/badforwr.fe | 12 ++++++++++++ fec/tests/own/bdefret.fe | 10 ++++++++++ fec/tests/own/okforrd.fe | 11 +++++++++++ 6 files changed, 68 insertions(+) create mode 100644 fec/tests/own/badforwr.fe create mode 100644 fec/tests/own/bdefret.fe create mode 100644 fec/tests/own/okforrd.fe diff --git a/SPEC.md b/SPEC.md index c0982be..9d4a4b8 100644 --- a/SPEC.md +++ b/SPEC.md @@ -589,6 +589,20 @@ pub fn main() -> !void { --- +### 7.9 작은 규칙들 + +구현자가 임의로 정하면 갈라지는 것들. 각각 한 줄이면 끝나므로 여기 모아 둔다. + +| | | +|---|---| +| `match` | **enum 값에만 쓴다.** 정수와 `char`에는 쓸 수 없으므로 완전성 검사가 배리언트 목록 하나로 정해진다. 정수 분기는 `if`/`else if`로 쓴다 | +| enum payload | `^T`처럼 소유하는 타입을 담을 수 있다. 다만 v0.1은 **활성 배리언트의 payload를 자동으로 놓아주지 않는다** — 담았다면 꺼내서 직접 놓아야 한다 | +| `for x in xs` | 순회는 `xs`를 **순회 동안 대여한다**. `x`가 그 안을 가리키는 참조이므로 몸통에서 `xs`에 쓰는 것은 R6 위반이다. 읽는 것은 공유 순회에서 허용된다 | +| `defer` 안의 `return` | **컴파일 에러.** 지연 블록은 함수가 무엇을 반환할지 이미 정한 뒤 스코프 정리 중에 돈다 | +| `undefined` 배열 | 초기화 추적은 **변수 단위이며 원소 단위가 아니다.** `undefined`로 선언한 배열은 선언 시점부터 쓰기 가능하고, 읽기 전에 무엇을 채웠는지는 검사하지 않는다. 슬라이스로 넘겨 채우는 것이 의도된 사용법이다 | +| 다른 유닛의 struct 리터럴 | 모든 필드를 명시해야 하므로 **`pub`이 아닌 필드가 하나라도 있으면 밖에서 리터럴을 쓸 수 없다.** 생성자 함수를 두어야 한다 | +| by-value `self` | `self`를 값으로 받아도 필드를 꺼내는 것은 R7 그대로 `mem.replace`가 필요하다. 예외는 자기 `drop` 안뿐이다 | + ## 8. 유닛 파일 하나가 유닛 하나다. 유닛의 canonical identity는 fully-qualified dotted unit path이며 diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index 8454791..d1f5c47 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -986,6 +986,11 @@ void check_stmt(FeCheckerState *s, FeNode *n) m7_check_match_stmt(s,n); break; case FE_N_RETURN: + /* A deferred block runs during scope cleanup, on the way out of a + function that has already decided what it returns. There is nothing + for a `return` in there to mean. */ + if (s->defer_depth != 0) + err(s->c, n->loc, "cannot return from inside defer"); expected=s->ret; if (n->a) stored=m7_check_expected(s,n->a,expected); diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index f8761ff..89eac2f 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -101,7 +101,18 @@ void check_for(FeCheckerState *s, FeNode *n) item_cname,n); n->cname=item_cname; } + /* Walking a container borrows it for the length of the walk: the + item is a reference into it, so writing the container underneath + would move what that reference points at (SPEC 5 R6). */ + if (iter_sym) + fe_own_access(s->c->diags,&iter_sym->own, + iter_mut ? FE_OWN_BORROW_MUT : FE_OWN_BORROW_SHARED, + n->loc); check_stmt(s,n->b); + if (iter_sym) { + if (iter_mut) fe_own_release_exclusive(&iter_sym->own); + else fe_own_release_shared(&iter_sym->own); + } s->scope=old; return; } @@ -459,6 +470,11 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) if (!s->loop_depth) err(c,n->loc,"break or continue outside loop"); break; case FE_N_RETURN: + /* A deferred block runs during scope cleanup, on the way out of a + function that has already decided what it returns. There is nothing + for a `return` in there to mean. */ + if (s->defer_depth != 0) + err(c, n->loc, "cannot return from inside defer"); b = n->a ? check_expr(s, n->a) : fe_type_intern(&c->types, "void"); if (s->ret && fe_own_is_reference_like(s->ret) && !own_return_from_allowed_root(s,n->a)) diff --git a/fec/tests/own/badforwr.fe b/fec/tests/own/badforwr.fe new file mode 100644 index 0000000..f3553e9 --- /dev/null +++ b/fec/tests/own/badforwr.fe @@ -0,0 +1,12 @@ +// ERROR:9:borrow +unit badforwr; + +// Walking a container borrows it: the item is a reference into it, so writing +// the container underneath moves what that reference points at. + +fn bad(xs: []mut i32) -> void { + for x in xs { + xs[0] = 9; + } + return; +} diff --git a/fec/tests/own/bdefret.fe b/fec/tests/own/bdefret.fe new file mode 100644 index 0000000..045b296 --- /dev/null +++ b/fec/tests/own/bdefret.fe @@ -0,0 +1,10 @@ +// ERROR:8:cannot return from inside defer +unit bdefret; + +// A deferred block runs during scope cleanup, on the way out of a function +// that has already decided what it returns. + +fn bad() -> i32 { + defer { return 1; } + return 0; +} diff --git a/fec/tests/own/okforrd.fe b/fec/tests/own/okforrd.fe new file mode 100644 index 0000000..3451761 --- /dev/null +++ b/fec/tests/own/okforrd.fe @@ -0,0 +1,11 @@ +unit okforrd; + +// A shared walk still allows reading -- of the item and of the container. + +fn ok(xs: []i32) -> i32 { + var sum: i32 = 0; + for x in xs { + sum = sum + x.^ + (xs.n as i32); + } + return sum; +} From ef7939f6095529c5232c5084ab5fc76747511482 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:12:53 +0900 Subject: [PATCH 173/184] =?UTF-8?q?GOAL=20P2:=20SPEC=20=EC=97=90=EC=84=9C?= =?UTF-8?q?=20=EC=A3=BD=EC=9D=80=20C=20=EB=B0=B1=EC=97=94=EB=93=9C?= =?UTF-8?q?=EB=A5=BC=20=EA=B1=B7=EC=96=B4=EB=82=B8=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파이프라인은 .fe → i386 asm → wasm → wlink → .exe 인데 명세는 아직 C 를 방출하는 컴파일러를 서술하고 있었다. 외부 감사가 그 문서를 충실히 읽고 존재하지 않는 문제(C 의 부호 있는 오버플로 UB)를 보고했다 -- 명세가 거짓말을 하면 그것을 읽는 사람이 틀린 답을 낸다. 걷어낸 것: .fei 심볼 파일과 그 위에 얹힌 증분 빌드 서술, fe_errors.h, --emit-c 와 --error-table, '호스트 C 방출', 'C 방출 시 static inline', '별도 C 표현'. 오류 코드 절은 실제대로 다시 썼다: 빌드 하나가 모든 유닛의 소스를 함께 읽고, 드라이버가 emit 전에 쓰인 이름을 모아 철자순으로 1부터 매긴다. 유닛 하나만 따로 코드 생성까지 밀고 갈 수 없다는 것도 그 결과로 적었다. R7 이 컴파일러 소스 파일 이름(own.c)을 대고 있던 것도 언어의 말로 바꿨고, 이동은 변수 단위이고 대여는 place 단위라는 구분을 붙였다. --target= 과 --model= 은 드라이버에서 없앴다. 타깃이 하나인데 받아들여서 무시하는 플래그는 안 받는 것보다 나쁘다. SPEC 이 약속만 하고 구현이 없는 것 셋을 TODO 에 적었다: --strip-error-names, fmt.fmt_error, 0b/0o 리터럴 값 계산. 240/240, 35/35. --- SPEC.md | 34 +++++++++++++--------------------- TODO.md | 3 +++ fec/src/driver.c | 5 ++++- 3 files changed, 20 insertions(+), 22 deletions(-) diff --git a/SPEC.md b/SPEC.md index 9d4a4b8..60baaa6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,7 +1,7 @@ # Ferro 언어 명세 v0.1.8 DOS용 시스템 프로그래밍 언어. C만큼 빠르고, 메모리 안전성을 함수 단위 지역 검사만으로 보장한다. -파일 확장자 `.fe`, 컴파일러 이름 `fec`, 심볼 파일 `.fei`. +파일 확장자 `.fe`, 컴파일러 이름 `fec`. 별도의 인터페이스 파일은 없다 — 빌드 하나가 모든 유닛의 소스를 함께 읽는다. 이 문서는 Ferro 언어 명세만 다룬다. 컴파일러 구현 지시서와 표준 라이브러리 상세 명세는 별도 문서에서 다룬다. 명세 판단이 애매한 부분은 §1 철학과 §5 소유권 규칙을 기준으로 결정한다. @@ -112,7 +112,7 @@ and or not orelse - `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. - **배타 대여를 호출에 넘기는 것은 이동이 아니라 그 호출 동안의 재대여다.** `&mut T`를 `&mut T` 파라미터에, `[]mut T`를 `[]mut T` 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는 동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해져서 `&mut`가 사실상 쓸 수 없게 된다. - `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. -- `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 C 표현은 없다. +- `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 표현은 없다. ### 4.3 구조체 @@ -196,25 +196,18 @@ fn read_all(path: str) -> !^[]u8 { // 표준 io는 core.Error로 통일 - 실패를 복구하지 않고 트랩으로 바꾸려면 `expr catch @trap()`을 쓴다. v0.1에는 별도 `must` 키워드를 두지 않는다. `error.Name`은 선언된 error 타입을 만들지 않고 기본 `core.Error`의 이름 있는 -멤버를 참조하는 익명 에러 값이다. 각 유닛은 사용한 이름을 `.fei`에 기록한다. -최종 빌드에서 **드라이버가 emit 단계 이전에** 모든 유닛의 `.fei`에서 사용된 이름을 -합치고 중복을 제거한 뒤 이름의 바이트순으로 정렬하여 1부터 `u16` 코드를 부여한다. -드라이버는 이 표를 단일 생성 헤더 `fe_errors.h`의 `#define` 정수 상수로 방출한다. -따라서 서로 다른 유닛의 `error.Name`은 같은 값이고 빌드 순서와 병렬 컴파일에도 -결과가 결정적이며 `switch` case 라벨로 쓸 수 있다. -이름 집합이 바뀌면 `fe_errors.h`와 그 헤더에 의존하는 오브젝트를 무효화하지만, -각 유닛의 `.c`는 재방출하지 않는다. 각 `.fei`는 그 unit이 사용하는 이름 집합만 -기록하므로 global 번호 재배정 때문에 다시 쓰지 않으며, source에서 이름 사용 자체가 -바뀐 unit의 `.fei` interface만 갱신한다. +멤버를 참조하는 익명 에러 값이다. **드라이버가 emit 단계 이전에** 빌드에 든 모든 +유닛에서 쓰인 이름을 모아 중복을 제거하고 이름의 바이트순으로 정렬하여 1부터 +`u16` 코드를 부여한다. 따라서 서로 다른 유닛의 `error.Name`은 같은 값이고, +빌드 순서와 무관하게 결과가 결정적이다. 빌드 디렉터리 이력에 따라 번호가 달라지는 append-only 표는 금지한다. -유닛 단위 `--emit-c`는 전체 이름 집합을 알 수 없으므로 `--error-table=<파일>`로 -확정된 표를 받아야 하며, 없으면 컴파일 에러다. -이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal +번호는 빌드 전체를 봐야 정해지므로 유닛 하나만 따로 코드 생성까지 밀고 갈 수는 +없다. 이름이 65,535개를 넘으면 컴파일 에러다. 명시적인 `error` 선언은 여전히 nominal 타입이며, 같은 멤버 이름이나 숫자 코드를 가진 다른 선언 및 `core.Error`와 자동 변환되지 않는다. `error.Name`의 타입은 `core.Error`이며 `core.Error!T` 또는 축약형 `!T`를 반환하는 함수에서만 직접 반환할 수 있다. `--strip-error-names`를 사용하면 실행 파일과 런타임 오류 문자열에서 이름을 -제거하지만 숫자 코드와 `.fei`의 타입/코드 일관성 정보는 유지한다. +제거하지만 숫자 코드는 유지한다. `fmt.fmt_error`는 이 정책에 따라 `core.Error` 값을 이름 또는 코드로 포맷한다. ### 4.7 타입 동등성과 alias @@ -280,7 +273,7 @@ r.^ = 1; // r의 마지막 사용 x += 1; // OK — 여기서 r의 대여는 이미 끝났다 ``` -**R7 (참조 무효화와 부분 이동).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. own.c는 변수 단위 상태만 추적하므로 field/index/`.?` projection에서 비-Copy 소유값을 이동해 꺼내는 것은 금지한다. `mem.replace(&mut place, replacement)`로 유효한 대체값을 남기면서 꺼내야 한다. 배열의 선택적 소유 원소는 `?^T`로 두고 `mem.replace(&mut arr[i], null).?`로 꺼낸다. projection chain 자체(`p.?.^`, `s.field.x`)는 값을 소비하지 않는다. +**R7 (참조 무효화와 부분 이동).** 참조 대상이 이동되거나 재대입되면 그 참조는 이후 사용 시 에러. 이동 상태는 변수 단위로만 추적하므로 field/index/`.?` projection에서 비-Copy 소유값을 이동해 꺼내는 것은 금지한다(대여는 §5 R6대로 place 단위로 갈라지지만, 이동은 그렇지 않다). `mem.replace(&mut place, replacement)`로 유효한 대체값을 남기면서 꺼내야 한다. 배열의 선택적 소유 원소는 `?^T`로 두고 `mem.replace(&mut arr[i], null).?`로 꺼낸다. projection chain 자체(`p.?.^`, `s.field.x`)는 값을 소비하지 않는다. 이 금지에는 예외가 하나 있다. **타입 자신의 `drop` 안에서는 `self`의 projection에서 값을 꺼낼 수 있다.** 그 객체는 사라지는 중이고 `drop`이 돌아간 뒤에 그것을 읽을 수 있는 코드가 없으므로, R7이 막으려는 "뒤에 남은 반쪽짜리 값"이 생기지 않는다. 다른 함수에서는 예외가 없다. @@ -295,7 +288,7 @@ x += 1; // OK — 여기서 r의 대여는 이미 끝났다 - `Static`: 문자열 리터럴 또는 `static`에서 파생되어 caller local borrow를 만들지 않는다. - `Param(N)`: 시그니처로 정해진 하나의 참조성 parameter에서 파생된다. 메서드는 `Param(self)`만 허용하며 다른 참조성 인자에서 파생되면 에러다. 자유 함수는 기존 규칙대로 참조성 parameter가 정확히 하나여야 한다. -control-flow 합류는 `Static + Static → Static`, `Static + Param(N) → Param(N)`, `Param(N) + Param(N) → Param(N)`이다. 서로 다른 `Param` provenance가 합류하면 컴파일 에러다. `?&T`/`?[]T`의 `null` 반환 경로는 caller borrow를 만들지 않는 경로이므로 static/null 경로와 `Param(N)` 경로가 합쳐지면 전체를 보수적으로 `Param(N)`으로 본다. 이 provenance는 함수 시그니처와 lowered `.fei` interface metadata에 기록할 수 있어야 한다. +control-flow 합류는 `Static + Static → Static`, `Static + Param(N) → Param(N)`, `Param(N) + Param(N) → Param(N)`이다. 서로 다른 `Param` provenance가 합류하면 컴파일 에러다. `?&T`/`?[]T`의 `null` 반환 경로는 caller borrow를 만들지 않는 경로이므로 static/null 경로와 `Param(N)` 경로가 합쳐지면 전체를 보수적으로 `Param(N)`으로 본다. 이 provenance는 함수 시그니처만 보고 결정할 수 있어야 한다. 호출 지점에서 `Param(N)` 결과는 **정해진 파생 원본을 대여한 것으로 취급**한다. 즉 결과를 지역 변수에 바인딩할 수 있으며, 그 대여가 사는 동안 원본에 R6·R7이 그대로 적용된다. `Static` 결과는 caller local borrow를 만들지 않는다. @@ -422,8 +415,7 @@ orelse_expr := expr 'orelse' expr 12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) ``` -- `and`, `or`는 단축 평가한다. 호스트 C 방출에서는 각각 `&&`, `||`로 - 매핑하며, 평가 순서와 단락 규칙은 Ferro 의미론을 그대로 유지한다. +- `and`, `or`는 단축 평가한다. 좌변이 답을 정하면 우변을 평가하지 않는다. - `orelse`와 `catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를 평가하지 않는다(§4.5·§4.6). - `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. @@ -789,7 +781,7 @@ binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다 | 튜플 / 다중 반환 | 편의 | 이름 없는 필드는 가독성 손해 | struct | | 레이블 있는 break | 편의 | — | 플래그 변수 | | 슬라이스 패턴 매칭 | 편의 | — | 인덱스 비교 | -| `inline fn` | 편의 | — | C 방출 시 `static inline` | +| `inline fn` | 편의 | — | 인라인 여부는 백엔드가 정한다 | | `must` 키워드 | 편의 | 실패를 트랩으로 바꾸는 문법 설탕일 뿐 핵심 의미론이 아님 | `expr catch @trap()` | | 블록 표현식 | 편의 | 값을 만드는 블록이 없으면 `catch`가 짧은 형태로 충분하고, 문법 표면이 작아진다 (§4.6) | `catch <식>`, `return`으로 탈출 | | 라이프타임 표기 (`'a`) | **구조적 불가** | 전역 분석 필요, R4를 풀어야 함 | R4 (2급 참조), R8 파생 반환, 인덱스 핸들 | diff --git a/TODO.md b/TODO.md index e5142df..d00362f 100644 --- a/TODO.md +++ b/TODO.md @@ -53,6 +53,9 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | `interrupt` `shared` `atomic` `critical` | 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | | lowering 미구현 진단 | `internal: cannot lower X`. 사용자 오류처럼 보이지 않는다 | | 배열·enum 페이로드의 자동 해제 | `release_at` 은 구조체 필드까지만 내려간다 | +| `--strip-error-names` | 받아들이지만 아무것도 하지 않는다 (SPEC §4.6) | +| `fmt.fmt_error` | 없다. SPEC §4.6 이 약속만 하고 있다 | +| `0b` / `0o` 리터럴 | 렉서는 받지만 값 계산이 10진과 16진만 안다 | --- diff --git a/fec/src/driver.c b/fec/src/driver.c index 2a6ce9b..5b721bd 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -60,7 +60,10 @@ int main(int argc, char **argv) else if(strcmp(argv[i],"-o")==0 && i+1 Date: Mon, 17 Aug 2026 16:19:30 +0900 Subject: [PATCH 174/184] =?UTF-8?q?GOAL=20P3-1/3-2:=20=ED=95=B8=EB=93=A4?= =?UTF-8?q?=EB=A1=9C=20=EB=8B=BF=EB=8A=94=20=EC=95=84=EB=A0=88=EB=82=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit R11 이 데이터가 데이터를 가리킬 때 내놓는 답이다. 아레나가 값을 소유하고 핸들은 숫자라서 참조가 아니고, R4 가 반대할 것이 없다. mem.Arena 옆에 두되 다른 자료구조다 -- 저쪽은 크기가 제각각인 것을 오프셋으로 나눠주고, 이쪽은 슬롯마다 고정된 T 를 담고 슬롯을 되받는다. 핸들은 믿는 것이 아니라 검사받는다. 슬롯의 세대, 아레나의 epoch, 어느 아레나에서 왔는지를 8바이트에 담아서, 놓아준 뒤에 쓰거나 reset 뒤에 쓰거나 다른 아레나에 물어보면 옆값이 아니라 null 이 나온다. 여덟 바이트는 빌드마다 같다 -- --no-checks 는 비교를 건너뛸 뿐 배치를 바꾸지 않는다. 세대가 다 닳은 슬롯은 재사용 목록에 넣지 않고 버린다. 한 바퀴 돌면 옛 핸들이 다시 유효해지는데, 그것이 세대가 막으려던 바로 그 일이다. get 은 사본을 돌려준다. 대여가 문장을 넘기지 않는 이유가 그것이고, 그래서 한 슬롯을 읽으면서 다른 슬롯에 쓸 수 있다. get_mut 은 두지 않았다 -- 아레나 하나를 통째로 잠그는 참조를 오래 들고 있게 하는 API 다. 길에서 고친 것: 메서드 호출이 시그니처를 호출자 유닛에서 풀고 있었다. 그래서 Arena(T) 안의 Handle(T) 를 부르는 쪽 유닛에서 찾다가 실패했다. 필드 타입에 쓰던 enter_declaring_unit 을 method_type 에도 물렸다. 241/241, 36/36. --- fec/src/check.c | 10 ++- fec/std/arena.fe | 166 +++++++++++++++++++++++++++++++++++++++ fec/tests/exec/arenat.fe | 81 +++++++++++++++++++ 3 files changed, 256 insertions(+), 1 deletion(-) create mode 100644 fec/std/arena.fe create mode 100644 fec/tests/exec/arenat.fe diff --git a/fec/src/check.c b/fec/src/check.c index 357b955..77bf9b7 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -445,7 +445,15 @@ FeType *method_type(FeCheck *c, FeNode *node, FeType *owner) (strcmp(node->text,"&")==0 || strcmp(node->text,"&mut")==0) && node->a && node->a->text && strcmp(node->a->text,"Self")==0) return fe_type_ref(&c->types,owner,strcmp(node->text,"&mut")==0); - return node_type(c,node); + /* The rest of a method's signature is written in the unit that declared + the type, so a name in it means what that unit means by it and not what + the caller happens to mean. */ + { + int back=enter_declaring_unit(c,owner ? owner->unit : 0); + FeType *t=node_type(c,node); + if (back>=0) enter_unit(c,(unsigned)back); + return t; + } } diff --git a/fec/std/arena.fe b/fec/std/arena.fe new file mode 100644 index 0000000..8fa5117 --- /dev/null +++ b/fec/std/arena.fe @@ -0,0 +1,166 @@ +unit std.arena; + +import std.mem; + +// An arena of `T` reached by handle, and the handle that reaches it. +// +// This is the shape SPEC R11 asks for when data points at data: the arena owns +// the values and a handle is a number, so nothing here is a reference and R4 +// has nothing to object to. `mem.Arena` next door is a different structure -- +// a block of bytes handed out by offset, for data whose size varies. This one +// holds a fixed `T` per slot and can take slots back. +// +// A handle is checked, not trusted. It carries the generation of the slot it +// was made from, the epoch of the arena, and which arena it came from, so +// using it after the slot was freed, after the arena was reset, or against +// some other arena all answer `null` rather than a neighbouring value. +// +// `T` is expected to be Copy -- integers, handles, small records. `get` +// answers with a copy, which is the whole reason a borrow never outlives the +// statement it was taken in. For a `T` that owns something, `take` moves it +// out and leaves a replacement (SPEC 5 R7). + +/// No slot. Every real index is smaller. +pub const NONE: u32 = 4294967295; + +/// Eight bytes, and the same eight in every build: `--no-checks` skips the +/// comparison, never the layout. A handle that changed shape with a flag +/// would not survive being written down. +pub struct Handle(T) { + pub index: u32, + /// `arena 8 | epoch 8 | generation 16` + pub tag: u32, + + pub fn none() -> Self { return Self{ index: NONE, tag: 0 }; } + + pub fn is_none(self: &Self) -> bool { return self.index == NONE; } + + pub fn same(self: &Self, other: Handle(T)) -> bool { + return self.index == other.index and self.tag == other.tag; + } +} + +struct Slot(T) { + value: T, + /// Bumped every time the slot is freed, so old handles stop matching. + gen: u32, + live: bool, + /// The next slot on the free list, or `NONE`. + next: u32, +} + +pub struct Arena(T) { + slots: ^[]mut Slot(T), + /// How many slots have ever been handed out; slots past this are untouched. + high: usize, + /// How many are live right now. + count: usize, + free: u32, + id: u32, + epoch: u32, + + /// `id` tells one arena from another in a handle. A program with a handful + /// of arenas numbers them itself; eight bits is more than that needs. + pub fn with_capacity(id: u32, n: usize) -> !Self { + let room: ^[]mut Slot(T) = try mem.alloc_slice(Slot(T), n); + return Self{ slots: room, high: 0, count: 0, free: NONE, + id: id % 256, epoch: 0 }; + } + + pub fn len(self: &Self) -> usize { return self.count; } + + pub fn room(self: &Self) -> usize { return self.slots.^.n; } + + fn tag_of(self: &Self, gen: u32) -> u32 { + return (self.id * 16777216) + (self.epoch * 65536) + gen; + } + + /// Is this handle still talking about a live slot in this arena? + pub fn valid(self: &Self, h: Handle(T)) -> bool { + if h.index == NONE { return false; } + if (h.index as usize) >= self.high { return false; } + if not self.slots.^[h.index as usize].live { return false; } + return self.slots.^[h.index as usize].gen == h.tag; + } + + pub fn alloc(self: &mut Self, v: T) -> !Handle(T) { + var at: usize = 0; + if self.free != NONE { + at = self.free as usize; + self.free = self.slots.^[at].next; + } else { + if self.high == self.slots.^.n { return error.OutOfMemory; } + at = self.high; + self.high = self.high + 1; + self.slots.^[at].gen = self.tag_of(0); + } + self.slots.^[at].value = v; + self.slots.^[at].live = true; + self.slots.^[at].next = NONE; + self.count = self.count + 1; + return Handle(T){ index: at as u32, tag: self.slots.^[at].gen }; + } + + /// A copy of what the slot holds, or nothing when the handle is stale. + /// The borrow of the arena ends with this statement, which is what lets a + /// caller read one slot while writing another. + pub fn get(self: &Self, h: Handle(T)) -> ?T { + if not self.valid(h) { return null; } + return self.slots.^[h.index as usize].value; + } + + /// Overwrite in place. Says whether the handle was good. + pub fn set(self: &mut Self, h: Handle(T), v: T) -> bool { + if not self.valid(h) { return false; } + self.slots.^[h.index as usize].value = v; + return true; + } + + /// Move the value out and leave `replacement` behind (SPEC 5 R7). This is + /// how a `T` that owns something leaves the arena. + pub fn take(self: &mut Self, h: Handle(T), replacement: T) -> ?T { + if not self.valid(h) { return null; } + return mem.replace(&mut self.slots.^[h.index as usize].value, + replacement); + } + + /// Exchange what two slots hold. Both handles have to be good. + pub fn swap(self: &mut Self, a: Handle(T), b: Handle(T)) -> bool { + if not self.valid(a) { return false; } + if not self.valid(b) { return false; } + let ai: usize = a.index as usize; + let bi: usize = b.index as usize; + if ai == bi { return true; } + let first: T = self.slots.^[ai].value; + let second: T = mem.replace(&mut self.slots.^[bi].value, first); + self.slots.^[ai].value = second; + return true; + } + + /// Give the slot back. Every handle to it stops matching. A slot whose + /// generation has run out is retired rather than reused -- wrapping around + /// would make an old handle valid again, which is the one thing the + /// generation is there to prevent. + pub fn free(self: &mut Self, h: Handle(T)) -> bool { + if not self.valid(h) { return false; } + let at: usize = h.index as usize; + self.slots.^[at].live = false; + self.count = self.count - 1; + let gen: u32 = self.slots.^[at].gen % 65536; + if gen == 65535 { return true; } + self.slots.^[at].gen = self.slots.^[at].gen + 1; + self.slots.^[at].next = self.free; + self.free = h.index; + return true; + } + + /// Forget everything at once. The epoch moves, so every handle made before + /// now is stale without having to touch a single slot. + pub fn reset(self: &mut Self) -> void { + self.epoch = (self.epoch + 1) % 256; + self.high = 0; + self.count = 0; + self.free = NONE; + return; + } +} diff --git a/fec/tests/exec/arenat.fe b/fec/tests/exec/arenat.fe new file mode 100644 index 0000000..41469bd --- /dev/null +++ b/fec/tests/exec/arenat.fe @@ -0,0 +1,81 @@ +// EXIT:0 +// OUTPUT:alloc 10 20 30 len 3 +// OUTPUT:freed len 2 stale -1 live 30 +// OUTPUT:reused index 1 old -1 new 99 +// OUTPUT:swap 99 10 +// OUTPUT:set 77 take 77 after 5 +// OUTPUT:reset len 0 before -1 +// OUTPUT:other -1 +// OUTPUT:full yes +// OUTPUT:balanced +unit arenat; + +import std.io; +import std.sys; +import std.arena; + +// SPEC R11: the arena owns the values and a handle is a number. The point of +// the number carrying a generation is that using it after the slot was given +// back answers `null` rather than whatever moved in afterwards. + +fn run() -> !void { + var a: arena.Arena(i32) = try arena.Arena(i32).with_capacity(1, 3); + let x: arena.Handle(i32) = try a.alloc(10); + let y: arena.Handle(i32) = try a.alloc(20); + let z: arena.Handle(i32) = try a.alloc(30); + @print("alloc {} {} {} len {}\n", a.get(x) orelse -1, a.get(y) orelse -1, + a.get(z) orelse -1, a.len()); + + // Give one back. Its handle stops meaning anything; the others do not. + let gone: bool = a.free(y); + @print("freed len {} stale {} live {}\n", a.len(), a.get(y) orelse -1, + a.get(z) orelse -1); + + // The slot comes back on the free list, and the old handle still does not + // match the new occupant. + let again: arena.Handle(i32) = try a.alloc(99); + @print("reused index {} old {} new {}\n", again.index, + a.get(y) orelse -1, a.get(again) orelse -1); + + let ok: bool = a.swap(x, again); + @print("swap {} {}\n", a.get(x) orelse -1, a.get(again) orelse -1); + + let wrote: bool = a.set(x, 77); + let took: i32 = a.take(x, 5) orelse -1; + @print("set {} take {} after {}\n", 77, took, a.get(x) orelse -1); + + // Reset moves the epoch, so every handle made before it is stale without + // a single slot being touched. + a.reset(); + @print("reset len {} before {}\n", a.len(), a.get(x) orelse -1); + + // A handle from one arena means nothing to another. + var b: arena.Arena(i32) = try arena.Arena(i32).with_capacity(2, 2); + let h: arena.Handle(i32) = try b.alloc(41); + @print("other {}\n", a.get(h) orelse -1); + + // Running out of room is an error, not a trap. + var full: bool = false; + fill(&mut b) catch |e| { full = true; }; + @print("full {}\n", yesno(full)); + return; +} + +/// Two more into an arena that has room for one. +fn fill(b: &mut arena.Arena(i32)) -> !void { + let p: arena.Handle(i32) = try b.alloc(1); + let q: arena.Handle(i32) = try b.alloc(2); + return; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} From 63baa4f5232435c9e79f774563aef02d745a9770 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:22:11 +0900 Subject: [PATCH 175/184] =?UTF-8?q?GOAL=20P3-3:=20List=20=EC=97=90=20pop?= =?UTF-8?q?=20take=20swap=20slice=20slice=5Fmut=20clear?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 컴파일러가 실제로 쓰는 나머지 표면이다. take 는 R7 대로 대체값을 남기고 꺼내므로 리스트에 구멍이 생기지 않는다. slice() 를 쓰려면 언어가 한 걸음 필요했다. &Self 로 읽어도 필드가 ^[]mut T 이니 슬라이스가 []mut T 로 나오는데, 선언한 반환은 []T 다. 호출 인자 자리의 약화만 있고 반환 자리에는 없었다. 반환 위치의 약화를 허용했다. R8 이 이미 그 파생을 허용한 뒤라면 []mut T 를 []T 로 넘기는 것은 가진 것보다 적게 넘기는 일이라 새 별칭을 만들지 않는다. &Self 메서드가 자기가 소유한 것의 읽기 전용 뷰를 내주는 길이 이것뿐이다. SPEC §4.2 에 적었고, let 은 여전히 안 된다는 것을 badletwk 가 고정한다. 243/243, 37/37. --- SPEC.md | 2 +- fec/src/check.c | 17 ++++++++++++ fec/src/checkcal.c | 3 +- fec/src/checkpri.h | 1 + fec/src/checkstm.c | 1 + fec/std/list.fe | 36 ++++++++++++++++++++++++ fec/tests/exec/listmor.fe | 55 +++++++++++++++++++++++++++++++++++++ fec/tests/types/badletwk.fe | 8 ++++++ fec/tests/types/okretwk.fe | 12 ++++++++ 9 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 fec/tests/exec/listmor.fe create mode 100644 fec/tests/types/badletwk.fe create mode 100644 fec/tests/types/okretwk.fe diff --git a/SPEC.md b/SPEC.md index 60baaa6..c6a1a9a 100644 --- a/SPEC.md +++ b/SPEC.md @@ -109,7 +109,7 @@ and or not orelse - **배열은 포인터로 붕괴하지 않는다.** 함수에 넘기려면 `arr[..]`로 슬라이스를 만들거나 `&arr` / `^[N]T`를 쓴다. - 슬라이싱: `arr[..]`, `arr[a..b]`(반개구간, 경계 검사), `arr[a..]`, `slice[a..b]`. `let` 배열·공유 슬라이스에서는 `[]T`, `var` 배열·배타 슬라이스에서는 `[]mut T`가 생긴다. -- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. +- `[]mut T`는 `[]T`로, `&mut T`는 `&T`로 **호출 인자 위치에서만** 암묵 재대여할 수 있다. 이것은 호출 동안의 read-only view이며 원래 배타 대여는 원래 마지막 사용까지 유지된다. 일반 `let`/대입에는 이 암묵 약화를 적용하지 않는다. **반환 위치에서도 약화할 수 있다** — `[]mut T`를 `[]T`로, `&mut T`를 `&T`로 반환하는 것은 R8이 이미 그 파생을 허용한 뒤에 가진 것보다 적게 넘기는 일이므로 새 별칭을 만들지 않는다. `&Self` 메서드가 자기가 소유한 것의 읽기 전용 뷰를 내주는 길이 이것뿐이다. 장기 shared borrow가 필요하면 root/place에서 명시적으로 새 `&` 또는 shared slice를 만들고 R6 검사를 받는다. - **배타 대여를 호출에 넘기는 것은 이동이 아니라 그 호출 동안의 재대여다.** `&mut T`를 `&mut T` 파라미터에, `[]mut T`를 `[]mut T` 파라미터에 넘기면 호출이 끝날 때 돌려받는다. 호출이 도는 동안 호출자는 그 값에 손댈 수 없으므로 별칭이 생기지 않는다. 이것이 없으면 배타 파라미터를 다시 넘기는 일이 함수당 한 번만 가능해져서 `&mut`가 사실상 쓸 수 없게 된다. - `^[]T`는 "슬라이스를 가리키는 포인터"가 아니라 길이를 함께 소유하는 독립 타입이다. R4의 일반 `^T` 대상 제한의 예외이며 `?^[]T`도 허용한다. `*[]T`/`*[]mut T`는 계속 금지한다. `mem.alloc_slice(T, n)`가 반환하고 drop 시 버퍼를 해제한다. - `str`은 nominal 타입이 아니라 미리 선언된 `const str = []u8;` type alias다. UTF-8 검증을 보장하지 않으며 문자열 리터럴은 정적 읽기 전용 `[]u8`이다. 따라서 별도 변환 규칙이나 별도 표현은 없다. diff --git a/fec/src/check.c b/fec/src/check.c index 77bf9b7..4240a4a 100644 --- a/fec/src/check.c +++ b/fec/src/check.c @@ -120,6 +120,23 @@ int call_reborrows(const FeType *param, const FeType *arg) return 0; } +/* Handing back less than you hold. R8 says a returned reference has to be + derived from a parameter or a static; given that, returning the shared form + of an exclusive one is safe -- the caller cannot do anything with `[]T` that + it could not do with `[]mut T`. Without this a method on `&Self` cannot hand + out a read-only view of what it owns. */ +int return_weakens(const FeType *want, const FeType *got) +{ + if (!want || !got) return 0; + if (want->kind==FE_TYPE_SLICE && got->kind==FE_TYPE_SLICE && + !want->ref_mut && got->ref_mut) + return fe_type_equal(want->elem,got->elem); + if (want->kind==FE_TYPE_REF && got->kind==FE_TYPE_REF && + !want->ref_mut && got->ref_mut) + return fe_type_equal(want->elem,got->elem); + return 0; +} + int explicit_castable(FeType *a, FeType *b) { if (!a || !b) return 0; diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index d1f5c47..b27b096 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -1016,7 +1016,8 @@ void check_stmt(FeCheckerState *s, FeNode *n) expected->error_value && expected->error_value->kind==FE_TYPE_VOID) { } else if (!fe_type_equal(expected,stored) && - !m7_actual_compatible(expected,stored,n->a)) + !m7_actual_compatible(expected,stored,n->a) && + !return_weakens(expected,stored)) err(s->c,n->loc,"return type mismatch"); if (n->a) mark_moved(s,n->a,actual); break; diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h index 531045c..9e59b2e 100644 --- a/fec/src/checkpri.h +++ b/fec/src/checkpri.h @@ -95,6 +95,7 @@ int in_own_drop(FeCheckerState *s, FeNode *n); void mark_moved(FeCheckerState *s, FeNode *n, FeType *t); int compatible(FeType *want, FeType *got, FeNode *value); int call_reborrows(const FeType *param, const FeType *arg); +int return_weakens(const FeType *want, const FeType *got); int explicit_castable(FeType *a, FeType *b); FeType *node_type(FeCheck *c, FeNode *n); char *unit_cname(FeCheck *c, const char *name); diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index 89eac2f..ae79b49 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -482,6 +482,7 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) mark_moved(s,n->a,b); if (known(b) && b->kind == FE_TYPE_VOID && s->ret->kind != FE_TYPE_VOID) err(c, n->loc, "void expression returned from value function"); + else if (return_weakens(s->ret,b)) { } else if (known(s->ret) && known(b) && !fe_type_equal(s->ret, b) && b->kind != FE_TYPE_UNKNOWN && !compatible(s->ret,b,n->a)) diff --git a/fec/std/list.fe b/fec/std/list.fe index cdf7c36..d9bb717 100644 --- a/fec/std/list.fe +++ b/fec/std/list.fe @@ -32,6 +32,42 @@ pub struct List(T) { return; } + /// Take the last one off. Nothing to take is `null`, not a trap. + pub fn pop(self: &mut Self) -> ?T { + if self.len == 0 { return null; } + self.len = self.len - 1; + return self.items.^[self.len]; + } + + /// Move one out and leave `replacement` where it was (SPEC 5 R7). This is + /// how a `T` that owns something leaves the list without the list ending + /// up with a hole in it. + pub fn take(self: &mut Self, i: usize, replacement: T) -> T { + return mem.replace(&mut self.items.^[i], replacement); + } + + /// Exchange two elements. + pub fn swap(self: &mut Self, i: usize, j: usize) -> void { + if i == j { return; } + let first: T = self.items.^[i]; + let second: T = mem.replace(&mut self.items.^[j], first); + self.items.^[i] = second; + return; + } + + /// The elements as a slice, so `for x in xs.slice()` walks them. R8(a): + /// derived from `self`, so the borrow belongs to the caller. + pub fn slice(self: &Self) -> []T { + return self.items.^[0..self.len]; + } + + pub fn slice_mut(self: &mut Self) -> []mut T { + return self.items.^[0..self.len]; + } + + /// Forget the elements and keep the buffer. + pub fn clear(self: &mut Self) -> void { self.len = 0; return; } + /// Move to a buffer twice the size. Kept apart from `push` because the /// borrow that hands over the old buffer must not be live while the old /// buffer is still being read (SPEC 5 R6). diff --git a/fec/tests/exec/listmor.fe b/fec/tests/exec/listmor.fe new file mode 100644 index 0000000..dda291e --- /dev/null +++ b/fec/tests/exec/listmor.fe @@ -0,0 +1,55 @@ +// EXIT:0 +// OUTPUT:walk 6 count 3 +// OUTPUT:swapped 3 1 +// OUTPUT:took 2 left 9 +// OUTPUT:pop 1 then 9 empty -1 +// OUTPUT:cleared 0 room 4 +// OUTPUT:balanced +unit listmor; + +import std.io; +import std.sys; +import std.list; + +// The rest of the List surface a compiler needs: walk it, exchange two, move +// one out and leave something valid behind, take the last off, and empty it +// without going back to the allocator. + +fn run() -> !void { + var xs: list.List(i32) = try list.List(i32).with_capacity(4); + try xs.push(1); + try xs.push(2); + try xs.push(3); + + // `slice()` is a shared view derived from `self` (SPEC 5 R8(a)), which is + // what lets `for` walk it. + var sum: i32 = 0; + for x in xs.slice() { sum = sum + x.^; } + @print("walk {} count {}\n", sum, xs.count()); + + xs.swap(0, 2); + @print("swapped {} {}\n", xs.at(0), xs.at(2)); + + // SPEC 5 R7: what leaves a projection leaves a replacement behind. + let old: i32 = xs.take(1, 9); + @print("took {} left {}\n", old, xs.at(1)); + + let a: i32 = xs.pop() orelse -1; + let b: i32 = xs.pop() orelse -1; + let c: i32 = xs.pop() orelse -1; + let d: i32 = xs.pop() orelse -1; + @print("pop {} then {} empty {}\n", a, b, d); + + try xs.push(7); + let room: usize = 4; + xs.clear(); + @print("cleared {} room {}\n", xs.count(), room); + return; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} diff --git a/fec/tests/types/badletwk.fe b/fec/tests/types/badletwk.fe new file mode 100644 index 0000000..c1c6d5c --- /dev/null +++ b/fec/tests/types/badletwk.fe @@ -0,0 +1,8 @@ +// ERROR:6:cannot rebind a mut borrow +unit badletwk; + +// 반환은 약화해도 `let` 은 여전히 안 된다 (SPEC §4.2). +fn bad(m: &mut i32) -> i32 { + let s: &i32 = m; + return s.^; +} diff --git a/fec/tests/types/okretwk.fe b/fec/tests/types/okretwk.fe new file mode 100644 index 0000000..f904f06 --- /dev/null +++ b/fec/tests/types/okretwk.fe @@ -0,0 +1,12 @@ +unit okretwk; + +// R8 이 파생을 허용한 뒤라면 가진 것보다 적게 넘기는 것은 안전하다. + +struct Buf { + items: ^[]mut i32, + + fn all(self: &Self) -> []i32 { return self.items.^[0..2]; } + fn all_mut(self: &mut Self) -> []mut i32 { return self.items.^[0..2]; } +} + +fn one(p: &mut i32) -> &i32 { return p; } From b7ce16f65e513b633ce5fbb7ec52c2ed86ec4ad4 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:27:47 +0900 Subject: [PATCH 176/184] =?UTF-8?q?GOAL=20P3-4:=20StringInterner,=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EA=B3=A0=20=EA=B7=B8=EA=B2=83=EC=9D=B4=20?= =?UTF-8?q?=EB=93=9C=EB=9F=AC=EB=82=B8=20=EB=B0=B0=EC=B9=98=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이름 하나당 사본 하나와 그것을 대신하는 숫자. 컴파일러는 이름을 끊임없이 비교하고 사방에 저장하는데, StrId 둘을 비교하는 것은 정수 둘을 비교하는 것이고 하나를 저장하는 것은 4바이트에 소유권 없음이다. str 을 꺼내는 API 를 일부러 두지 않았다. 텍스트를 빌리는 것은 interner 를 빌리는 것인데, interner 는 이름을 든 채로 계속 더 넣고 싶은 바로 그 물건이다 -- 파서는 식별자를 읽으면서 같은 숨에 다음 것을 등록한다. 텍스트를 열어서 하려던 일은 전부 여기 있다: eq, len_of, hash_of, find, copy_into. 그리고 이것이 제네릭 인스턴스를 필드로 담는 구조체를 통째로 깨뜨리던 버그를 드러냈다. Holder{ bytes: ^[]mut u8, used: usize, seen: map.Map(u32) } 36 바이트여야 하는데 12 로 잡혔다. Map(u32) 를 짓는 중에 그 안의 Slot(u32) 를 인스턴스화하면 거기서 배치 패스가 다시 돈다. 그때 Map(u32) 는 field_count 는 4 인데 필드 배열이 아직 아무것도 말하지 않는 상태라, 크기 0 으로 확정되고 굳었다. 이미 크기가 있는 타입은 아무도 다시 계산하지 않으니 Holder 는 그 0 을 읽었다. 셋을 고쳤다: 짓는 중인 인스턴스는 building 을 세워 배치를 거절하고, 멤버가 아직 자리를 못 잡은 집합 타입은 틀린 답으로 굳느니 물러나며, 배치 패스는 움직임이 없을 때까지 돈다. 245/245, 38/38. --- fec/src/checkgen.c | 2 + fec/src/types.c | 41 +++++++++++- fec/src/types.h | 4 ++ fec/std/intern.fe | 131 ++++++++++++++++++++++++++++++++++++++ fec/tests/exec/interns.fe | 62 ++++++++++++++++++ 5 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 fec/std/intern.fe create mode 100644 fec/tests/exec/interns.fe diff --git a/fec/src/checkgen.c b/fec/src/checkgen.c index 619213f..e2432df 100644 --- a/fec/src/checkgen.c +++ b/fec/src/checkgen.c @@ -210,6 +210,7 @@ FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, t=fe_type_intern_unit(&c->types,home->name,key); if (!t || t->kind!=FE_TYPE_UNKNOWN) return t; t->kind=FE_TYPE_STRUCT; + t->building=1; t->packed=(decl->flags & FE_NODE_PACKED)!=0; t->decl_node=decl; t->bind_count=0; @@ -246,6 +247,7 @@ FeType *build_struct_instance(FeCheck *c, FeUnit *home, FeNode *decl, pop_bindings(c,&save); c->types.unit_name=save_unit; } + t->building=0; fe_type_layout_all(&c->types); /* A type that says how to let go of itself needs that method to exist for every instance, whether or not anyone calls it by name: scope cleanup diff --git a/fec/src/types.c b/fec/src/types.c index b7b7cff..46bb024 100644 --- a/fec/src/types.c +++ b/fec/src/types.c @@ -48,6 +48,7 @@ static FeType *new_type(FeTypeCtx *ctx, const char *name, FeTypeKind kind) t->emit_state = 0; t->cycle_state = 0; t->layout_state = 0; + t->building = 0; ctx->types = t; return t; } @@ -475,6 +476,28 @@ static void leave_decl_unit(FeTypeCtx *ctx, int back, const char *was) ctx->unit_name = was; } +/* Did every field end up with a size? A struct whose members are not settled + cannot be settled either -- and freezing it here is worse than leaving it, + because nothing recomputes a type that already has a size. */ +static int members_ready(const FeType *t) +{ + unsigned i; + unsigned j; + if (t->kind == FE_TYPE_STRUCT) { + for (i = 0; i < t->field_count; ++i) { + if (!t->fields[i].type) return 0; + if (t->fields[i].type->layout_state != 2) return 0; + } + return 1; + } + for (i = 0; i < t->variant_count; ++i) + for (j = 0; j < t->variants[i].field_count; ++j) { + if (!t->variants[i].fields[j].type) return 0; + if (t->variants[i].fields[j].type->layout_state != 2) return 0; + } + return 1; +} + static void layout_type(FeTypeCtx *ctx, FeType *t) { unsigned i; @@ -483,6 +506,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) unsigned long max_size; unsigned max_align; if (!t || t->size) return; + if (t->building) return; if (t->layout_state == 1) { t->size = 1; t->align = 1; @@ -562,6 +586,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) t->fields[i].offset = off; off += fe_type_size(t->fields[i].type); } + if (!members_ready(t)) { t->layout_state = 0; return; } t->align = max_align; t->size = round_up(off, max_align); t->layout_state = 2; @@ -594,6 +619,7 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) } if (off > max_size) max_size = off; } + if (!members_ready(t)) { t->layout_state = 0; return; } t->bits = t->variant_count > 256U ? 16U : 8U; off = round_up(t->bits / 8U, max_align); t->size = round_up(off + max_size, max_align); @@ -605,7 +631,20 @@ static void layout_type(FeTypeCtx *ctx, FeType *t) void fe_type_layout_all(FeTypeCtx *ctx) { FeType *t; - for (t = ctx->types; t; t = t->next) layout_type(ctx, t); + int again = 1; + unsigned rounds = 0; + /* One pass settles a type only if everything under it is already settled, + so a type that had to wait is picked up by the next round. Sixteen is + far past any real nesting; it is here so a cycle cannot spin. */ + while (again && rounds < 16U) { + again = 0; + for (t = ctx->types; t; t = t->next) { + if (t->size || t->layout_state == 2) continue; + layout_type(ctx, t); + if (t->layout_state == 2) again = 1; + } + ++rounds; + } } FeFieldType *fe_type_field(FeType *t, const char *name) diff --git a/fec/src/types.h b/fec/src/types.h index d8db1ea..c15d9a3 100644 --- a/fec/src/types.h +++ b/fec/src/types.h @@ -103,6 +103,10 @@ struct FeType { marker made a struct in the middle of the first look complete to the second -- one byte wide, with every field on top of the next. */ int layout_state; + /* Set while a generic instance is being filled in. Its field array + exists but says nothing yet, and a size taken from it would be + wrong and would then be frozen. */ + int building; }; typedef struct FeTypeCtx { diff --git a/fec/std/intern.fe b/fec/std/intern.fe new file mode 100644 index 0000000..03ddba8 --- /dev/null +++ b/fec/std/intern.fe @@ -0,0 +1,131 @@ +unit std.intern; + +import std.map; +import std.mem; + +// One copy of every distinct name, and a number that stands for it. +// +// A compiler compares names constantly and stores them everywhere. Comparing +// two `StrId` is comparing two integers; storing one costs four bytes and no +// ownership. That is the whole point. +// +// There is deliberately no way to get a `str` back out. A borrow of the text +// would be a borrow of the interner, and the interner is exactly the thing you +// want to keep adding to while holding names -- the parser reads an identifier +// and registers the next one in the same breath. Everything you would open the +// text for is here instead: compare it, measure it, hash it, write it. + +/// A name, as a number. Copy, four bytes, and meaningless to any other +/// interner -- which is fine, because a program has one. +pub struct StrId { + pub raw: u32, + + pub fn same(self: &Self, other: StrId) -> bool { + return self.raw == other.raw; + } +} + +/// No name. +pub const NONE: u32 = 4294967295; + +struct Entry { + at: usize, + len: usize, +} + +pub struct Interner { + /// Every name end to end. Nothing is ever removed, so an offset stays + /// good for as long as the interner does. + bytes: ^[]mut u8, + used: usize, + names: ^[]mut Entry, + count: usize, + /// Text to id, so interning the same name twice gives the same number. + seen: map.Map(u32), + + pub fn with_capacity(n: usize) -> !Self { + let text: ^[]mut u8 = try mem.alloc_slice(u8, n * 8); + let table: ^[]mut Entry = try mem.alloc_slice(Entry, n); + let index: map.Map(u32) = try map.Map(u32).with_capacity(n); + return Self{ bytes: text, used: 0, names: table, count: 0, + seen: index }; + } + + pub fn count_of(self: &Self) -> usize { return self.count; } + + /// The number for this name, making one if it is new. + pub fn intern(self: &mut Self, text: []u8) -> !StrId { + let found: u32 = self.seen.get(text, NONE); + if found != NONE { return StrId{ raw: found }; } + if self.count == self.names.^.n { return error.OutOfMemory; } + let at: usize = self.used; + if at + text.n > self.bytes.^.n { return error.OutOfMemory; } + var i: usize = 0; + while i < text.n { + self.bytes.^[at + i] = text[i]; + i = i + 1; + } + let id: usize = self.count; + self.names.^[id].at = at; + self.names.^[id].len = text.n; + self.used = at + text.n; + self.count = id + 1; + try self.seen.put(text, id as u32); + return StrId{ raw: id as u32 }; + } + + /// Is this a name it has seen? `NONE` when not. + pub fn find(self: &Self, text: []u8) -> u32 { + return self.seen.get(text, NONE); + } + + pub fn len_of(self: &Self, id: StrId) -> usize { + if (id.raw as usize) >= self.count { return 0; } + return self.names.^[id.raw as usize].len; + } + + /// Does this id spell this text? The comparison every `if name == "fn"` + /// in a parser turns into. + pub fn eq(self: &Self, id: StrId, text: []u8) -> bool { + if (id.raw as usize) >= self.count { return false; } + let e: usize = id.raw as usize; + if self.names.^[e].len != text.n { return false; } + var i: usize = 0; + while i < text.n { + if self.bytes.^[self.names.^[e].at + i] != text[i] { return false; } + i = i + 1; + } + return true; + } + + /// FNV-1a over the stored bytes, for anything that wants to bucket names + /// without opening them. + pub fn hash_of(self: &Self, id: StrId) -> u32 { + if (id.raw as usize) >= self.count { return 0; } + let e: usize = id.raw as usize; + var h: u32 = 2166136261; + var i: usize = 0; + while i < self.names.^[e].len { + h = h ^ (self.bytes.^[self.names.^[e].at + i] as u32); + h = h * 16777619; + i = i + 1; + } + return h; + } + + /// Copy the name into `out` and say how many bytes it took. This is how a + /// name reaches a diagnostic without the interner being borrowed past the + /// statement. + pub fn copy_into(self: &Self, id: StrId, out: []mut u8) -> usize { + if (id.raw as usize) >= self.count { return 0; } + let e: usize = id.raw as usize; + var n: usize = self.names.^[e].len; + if n > out.n { n = out.n; } + var i: usize = 0; + while i < n { + out[i] = self.bytes.^[self.names.^[e].at + i]; + i = i + 1; + } + return n; + } +} diff --git a/fec/tests/exec/interns.fe b/fec/tests/exec/interns.fe new file mode 100644 index 0000000..dfcbb62 --- /dev/null +++ b/fec/tests/exec/interns.fe @@ -0,0 +1,62 @@ +// EXIT:0 +// OUTPUT:ids 0 1 2 count 3 +// OUTPUT:again 0 same yes count 3 +// OUTPUT:eq yes no len 4 +// OUTPUT:find 1 missing yes +// OUTPUT:hash steady yes apart yes +// OUTPUT:copied unit 4 +// OUTPUT:balanced +unit interns; + +import std.io; +import std.sys; +import std.intern; + +// One copy of every distinct name, and a number that stands for it. Comparing +// two names is comparing two integers; storing one costs four bytes and no +// ownership. +// +// There is deliberately no way to get a `str` back out: a borrow of the text +// would be a borrow of the interner, and the interner is exactly what a parser +// wants to keep adding to while it holds names. + +fn run() -> !void { + var t: intern.Interner = try intern.Interner.with_capacity(8); + let a: intern.StrId = try t.intern("unit"); + let b: intern.StrId = try t.intern("fn"); + let c: intern.StrId = try t.intern("struct"); + @print("ids {} {} {} count {}\n", a.raw, b.raw, c.raw, t.count_of()); + + // The same name twice is the same number, and costs nothing new. + let again: intern.StrId = try t.intern("unit"); + @print("again {} same {} count {}\n", again.raw, yesno(a.same(again)), + t.count_of()); + + @print("eq {} {} len {}\n", yesno(t.eq(a, "unit")), yesno(t.eq(a, "fn")), + t.len_of(a)); + + @print("find {} missing {}\n", t.find("fn"), + yesno(t.find("nope") == intern.NONE)); + + // A name hashes the same every time, and two names do not collide here. + @print("hash steady {} apart {}\n", yesno(t.hash_of(a) == t.hash_of(again)), + yesno(t.hash_of(a) != t.hash_of(b))); + + // The only way to see the text: copy it somewhere you own. + var buf: [8]u8 = undefined; + let n: usize = t.copy_into(a, buf[..]); + @print("copied {} {}\n", buf[0..n], n); + return; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} + +fn main() -> i32 { + run() catch |e| { @print("failed\n"); return 1; }; + if sys.allocs() == sys.frees() { @print("balanced\n"); } + else { @print("leaked\n"); } + return 0; +} From b0cc737c6be87180c9dc0a78334a1b8126560028 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:28:47 +0900 Subject: [PATCH 177/184] =?UTF-8?q?GOAL=20P3-5:=20StrId=20=EB=A5=BC=20?= =?UTF-8?q?=ED=82=A4=EB=A1=9C=20--=20=EB=B3=84=EB=8F=84=20IntMap=20?= =?UTF-8?q?=EC=9D=80=20=ED=95=84=EC=9A=94=20=EC=97=86=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std.map 은 바이트 열을 키로 받는다. 이름의 번호를 네 바이트로 써 내려놓으면 그대로 심볼 표가 된다. 감사가 권한 IntMap(V) 를 따로 만들 이유가 없고, 그 쪽이 trait 없는 v0.1 과도 덜 싸운다. intern.key_of 가 그 네 바이트를 써준다. 리졸버가 스코프마다 할 일이라 손으로 풀게 두지 않았다. scope x 10 y 20 of 2 245/245, 38/38. --- fec/std/intern.fe | 11 +++++++++++ fec/tests/exec/interns.fe | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/fec/std/intern.fe b/fec/std/intern.fe index 03ddba8..613b9b1 100644 --- a/fec/std/intern.fe +++ b/fec/std/intern.fe @@ -28,6 +28,17 @@ pub struct StrId { /// No name. pub const NONE: u32 = 4294967295; +/// A name written out as map key bytes. `std.map` keys on bytes, so a name +/// used as a key is just its number -- four of them, and nothing allocated. +/// A symbol table is `Map(V)` keyed on this. +pub fn key_of(id: StrId, out: []mut u8) -> []u8 { + out[0] = (id.raw % 256) as u8; + out[1] = ((id.raw / 256) % 256) as u8; + out[2] = ((id.raw / 65536) % 256) as u8; + out[3] = ((id.raw / 16777216) % 256) as u8; + return out[0..4]; +} + struct Entry { at: usize, len: usize, diff --git a/fec/tests/exec/interns.fe b/fec/tests/exec/interns.fe index dfcbb62..af68a10 100644 --- a/fec/tests/exec/interns.fe +++ b/fec/tests/exec/interns.fe @@ -5,12 +5,14 @@ // OUTPUT:find 1 missing yes // OUTPUT:hash steady yes apart yes // OUTPUT:copied unit 4 +// OUTPUT:scope x 10 y 20 of 2 // OUTPUT:balanced unit interns; import std.io; import std.sys; import std.intern; +import std.map; // One copy of every distinct name, and a number that stands for it. Comparing // two names is comparing two integers; storing one costs four bytes and no @@ -46,6 +48,15 @@ fn run() -> !void { var buf: [8]u8 = undefined; let n: usize = t.copy_into(a, buf[..]); @print("copied {} {}\n", buf[0..n], n); + + // A symbol table is a Map keyed on the name's number. std.map keys on + // bytes, so no separate integer-keyed map is needed. + var scope: map.Map(i32) = try map.Map(i32).with_capacity(8); + var key: [4]u8 = undefined; + try scope.put(intern.key_of(a, key[..]), 10); + try scope.put(intern.key_of(b, key[..]), 20); + @print("scope x {} y {} of {}\n", scope.get(intern.key_of(a, key[..]), -1), + scope.get(intern.key_of(b, key[..]), -1), scope.count_of()); return; } From f06a12f3416413dacaa50154126aebf8b9f680df Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:31:57 +0900 Subject: [PATCH 178/184] =?UTF-8?q?GOAL=20P4:=20--report-unsafe=20?= =?UTF-8?q?=EC=99=80=20--report-instances,=20=EA=B7=B8=EB=A6=AC=EA=B3=A0?= =?UTF-8?q?=20=EC=98=88=EC=82=B0=EC=9D=84=20CI=20=EC=97=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'ffec 에서 unsafe 가 몇 군데인가' 는 좋은 지표인데 세는 방법이 없으면 지표가 아니다. 이제 센다. unit unsafe *T unchecked std.io 6 1 0 std.sys 10 12 0 total 16 13 0 outside std 0 0 0 Ferro 렉서와 파서, interner, 아레나, 맵을 쓰는 프로그램 전부가 std 바깥에서 0 이다. 목표치를 이미 지키고 있었던 셈인데, 그것을 아무도 확인할 수 없었다. run.py 가 그 숫자를 검사한다. std.mem 과 std.sys 밖의 unsafe 와 *T 는 검사기가 약속한 것에 뚫린 구멍이므로 0 을 유지해야 하고, 늘어나면 알아채는 것이 아니라 빌드가 실패해야 한다. unsafe 블록 하나를 넣어 실패하는 것까지 확인했다. --report-instances 는 제네릭이 무엇이 됐는지 센다. interns.fe 는 20 인스턴스, 타입 4 개에 저장소 80 바이트, 메서드 16 개다. 245/245, 38/38. --- fec/src/driver.c | 10 ++++ fec/src/report.c | 120 +++++++++++++++++++++++++++++++++++++++++++++++ fec/src/report.h | 13 +++++ tests/run.py | 35 +++++++++++++- 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 fec/src/report.c create mode 100644 fec/src/report.h diff --git a/fec/src/driver.c b/fec/src/driver.c index 5b721bd..bc78d11 100644 --- a/fec/src/driver.c +++ b/fec/src/driver.c @@ -3,6 +3,7 @@ #include "resolve.h" #include "lower.h" #include "x86.h" +#include "report.h" #include #include #include @@ -19,6 +20,7 @@ static char *read_file(const char *name, unsigned long *size) static void usage(void) { puts("usage: fec [--dump-tokens|--dump-ast|--check|--dump-ir|--emit-asm] file.fe [-o out.asm] [--std=dir] [--no-checks]"); + puts(" fec [--report-unsafe|--report-instances] file.fe [--std=dir]"); } static void dump_tokens(const char *src, unsigned long n, const char *file, @@ -40,6 +42,7 @@ static void dump_tokens(const char *src, unsigned long n, const char *file, int main(int argc, char **argv) { int i,dump=0,dump_tok=0,check_only=0,no_checks=0,dump_ir=0,emit_asm=0; + int rep_unsafe=0,rep_inst=0; const char *file=0; const char *out_path=0; const char *std_root=0; @@ -63,6 +66,8 @@ int main(int argc, char **argv) /* One target (SPEC 2), so --target= and --model= are gone: a flag that is accepted and does nothing is worse than one that is not accepted at all. */ + else if(strcmp(argv[i],"--report-unsafe")==0) rep_unsafe=1; + else if(strcmp(argv[i],"--report-instances")==0) rep_inst=1; else if(strcmp(argv[i],"--strip-error-names")==0) { } else if(argv[i][0]!='-') file=argv[i]; else if(strcmp(argv[i],"--help")==0){usage();return 0;} @@ -103,6 +108,11 @@ int main(int argc, char **argv) if(ok){ fe_check_init(&check,&build,&d,pointer_bits,no_checks); if(!fe_check_program(&check)) ok=0; + /* Reports describe the program that was checked, so they come + after checking and instead of code generation. */ + if(rep_unsafe) fe_report_unsafe(&build,stdout); + if(rep_inst) fe_report_instances(&check,stdout); + if(rep_unsafe||rep_inst) { dump_ir=0; emit_asm=0; } if(ok && (dump_ir||emit_asm)){ FeIrModule ir; fe_ir_module_init(&ir); diff --git a/fec/src/report.c b/fec/src/report.c new file mode 100644 index 0000000..90ca99f --- /dev/null +++ b/fec/src/report.c @@ -0,0 +1,120 @@ +#include "report.h" +#include + +/* What `--report-unsafe` and `--report-instances` print. + * + * Both answer a question that is easy to ask and easy to let slide: how much of + * the program is outside what the checker can promise, and how much code the + * generic instances are about to become. A number nobody can produce is not a + * budget, so these are here rather than in a comment somewhere. */ + +typedef struct Counts { + unsigned unsafe_blocks; + unsigned raw_types; + unsigned unchecked_calls; +} Counts; + +/* Does this name end in `_unchecked`? Those are the deliberate holes in the + checked surface, and they are worth counting separately from `unsafe` + because they do not need a block around them. */ +static int is_unchecked(const char *name) +{ + unsigned long n; + unsigned long m = 10UL; /* strlen("_unchecked") */ + if (!name) return 0; + n = (unsigned long)strlen(name); + if (n < m) return 0; + return strcmp(name + (n - m), "_unchecked") == 0; +} + +static void walk(const FeNode *n, Counts *c) +{ + const FeNode *x; + if (!n) return; + if (n->kind == FE_N_UNSAFE) ++c->unsafe_blocks; + if (n->kind == FE_N_TYPE && n->text && strcmp(n->text, "*") == 0) + ++c->raw_types; + if (n->kind == FE_N_CALL) { + const char *callee = n->text; + if (!callee && n->a) { + if (n->a->kind == FE_N_IDENT) callee = n->a->text; + else if (n->a->kind == FE_N_MEMBER && n->a->b) + callee = n->a->b->text; + } + if (is_unchecked(callee)) ++c->unchecked_calls; + } + walk(n->a, c); + walk(n->b, c); + walk(n->c, c); + for (x = n->children; x; x = x->next) walk(x, c); +} + +/* The standard library is where the unchecked things are supposed to live, so + it is reported but kept out of the total a program is judged on. */ +static int is_std(const char *unit) +{ + return unit && strncmp(unit, "std.", 4) == 0; +} + +void fe_report_unsafe(const FeBuild *build, FILE *out) +{ + unsigned u; + Counts total; + Counts outside; + total.unsafe_blocks = 0; total.raw_types = 0; total.unchecked_calls = 0; + outside = total; + fprintf(out, "%-20s %8s %8s %10s\n", "unit", "unsafe", "*T", "unchecked"); + for (u = 0; u < build->count; ++u) { + const FeUnit *unit = &build->units[u]; + Counts c; + c.unsafe_blocks = 0; c.raw_types = 0; c.unchecked_calls = 0; + walk(unit->ast.root, &c); + if (!c.unsafe_blocks && !c.raw_types && !c.unchecked_calls) continue; + fprintf(out, "%-20s %8u %8u %10u\n", unit->name, c.unsafe_blocks, + c.raw_types, c.unchecked_calls); + total.unsafe_blocks += c.unsafe_blocks; + total.raw_types += c.raw_types; + total.unchecked_calls += c.unchecked_calls; + if (!is_std(unit->name)) { + outside.unsafe_blocks += c.unsafe_blocks; + outside.raw_types += c.raw_types; + outside.unchecked_calls += c.unchecked_calls; + } + } + fprintf(out, "%-20s %8u %8u %10u\n", "total", total.unsafe_blocks, + total.raw_types, total.unchecked_calls); + fprintf(out, "%-20s %8u %8u %10u\n", "outside std", outside.unsafe_blocks, + outside.raw_types, outside.unchecked_calls); +} + +void fe_report_instances(const FeCheck *c, FILE *out) +{ + unsigned i; + unsigned types = 0; + unsigned methods = 0; + unsigned long bytes = 0; + fprintf(out, "%-52s %6s %8s\n", "instance", "kind", "size"); + for (i = 0; i < c->instance_count; ++i) { + const FeInstance *inst = &c->instances[i]; + unsigned long size = 0; + if (inst->owner) ++methods; + else { + ++types; + /* A struct instance is code only through its methods; what it + costs on its own is the storage one value of it takes. */ + { + const FeType *t; + for (t = c->types.types; t; t = t->next) + if (t->name[0] && !strcmp(t->name, inst->key)) { + size = t->size; + break; + } + } + bytes += size; + } + fprintf(out, "%-52s %6s %8lu\n", inst->key, + inst->owner ? "method" : "type", size); + } + fprintf(out, "\n%u instances: %u types (%lu bytes of storage), %u methods\n", + c->instance_count, types, bytes, methods); +} diff --git a/fec/src/report.h b/fec/src/report.h new file mode 100644 index 0000000..d7546f2 --- /dev/null +++ b/fec/src/report.h @@ -0,0 +1,13 @@ +#ifndef FE_REPORT_H +#define FE_REPORT_H + +#include "check.h" +#include + +/* How much of the build is outside what the checker promises. */ +void fe_report_unsafe(const FeBuild *build, FILE *out); + +/* What the generic instances came to. */ +void fe_report_instances(const FeCheck *c, FILE *out); + +#endif diff --git a/tests/run.py b/tests/run.py index 913c2fd..34b3661 100644 --- a/tests/run.py +++ b/tests/run.py @@ -34,7 +34,7 @@ FIXTURES = ROOT / "fec" / "tests" WATCOM = ROOT / ".dosboxx" / "watcom" SOURCES = ("arena", "diag", "lexer", "ast", "parser", "types", "m7", "own", "check", "checkexp", "checkstm", "checkgen", "checkcal", "checkpro", - "resolve", "ir", "lower", "lowerprn", "lowerexp", "lowerstm", "x86", "driver") + "resolve", "ir", "lower", "lowerprn", "lowerexp", "lowerstm", "x86", "report", "driver") MARKER = re.compile(r"^//\s*ERROR:(?:(\d+):)?(.*)$") @@ -132,8 +132,41 @@ def main() -> int: marked = sum(1 for p in cases if expectation(p).line is not None) print(f"\n{len(cases) - len(failed)}/{len(cases)} passed " f"({marked} pin a line and message)") + if not args.select: + leak = unsafe_budget(fec) + if leak: + print(leak) + return 1 return 1 if failed else 0 +# The programs the budget is measured on: the Ferro front end, and the ones +# that lean hardest on the standard library. +BUDGETED = ("exec/lexer/tree.fe", "exec/interns.fe", "exec/arenat.fe", + "exec/maps.fe", "exec/wordfreq.fe") + + +def unsafe_budget(fec: Path) -> str: + """`unsafe` and `*T` belong to std.mem and std.sys. Anywhere else they are + a hole in what the checker promises, so the count outside std has to stay + at zero and a regression has to fail the build rather than be noticed.""" + for rel in BUDGETED: + path = FIXTURES / rel + if not path.is_file(): + return f"budget: {rel} is gone" + done = subprocess.run([str(fec), "--report-unsafe", str(path), + f"--std={ROOT / 'fec'}"], + capture_output=True, text=True) + line = [l for l in done.stdout.splitlines() + if l.startswith("outside std")] + if not line: + return f"budget: no report for {rel}\n{done.stdout}{done.stderr}" + counts = line[0].split()[2:] + if any(c != "0" for c in counts): + return (f"budget: {rel} has unsafe/raw pointers outside std: " + f"{line[0]}") + return "" + + if __name__ == "__main__": raise SystemExit(main()) From 3d226d890b61ea4341b14254a405cd8b5eaf3f3f Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:33:25 +0900 Subject: [PATCH 179/184] =?UTF-8?q?docs:=20GOAL=20=EC=9D=84=20=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=EB=A1=9C,=20TODO=20=EB=A5=BC=20=EC=A7=80=EA=B8=88=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0~P4 가 끝났다. GOAL.md 는 계획이 아니라 무엇이 나왔는지를 적는다 -- 특히 각 항목이 어떤 fixture 로 고정됐는지. TODO 의 셀프호스팅 목록을 다시 매겼다. 다음은 리졸버를 Ferro 로이고, 도구는 다 있다: intern, map, Node.bind, Map.clear. 정해진 것에 셋을 더했다: 정수 리터럴의 타입, store 폭은 자리가 정한다는 것, 그리고 std 밖 unsafe 예산이 0 이며 run.py 가 그것을 검사한다는 것. 245/245, 38/38. --- GOAL.md | 130 +++++++++++++++++++++++++------------------------------- 1 file changed, 59 insertions(+), 71 deletions(-) diff --git a/GOAL.md b/GOAL.md index c2cfe0b..cbaad00 100644 --- a/GOAL.md +++ b/GOAL.md @@ -1,100 +1,88 @@ # GOAL — 부트스트랩까지 남은 작업 외부 스펙 감사(v0.1.8)와, 그 항목들을 실제로 빌드해서 확인한 결과를 합친 실행 -계획이다. 감사는 문서를 읽었고 여기 적힌 것은 컴파일러에 물어본 답이다. +계획이었다. **P0~P4 는 전부 끝났다.** 감사는 문서를 읽었고 여기 적힌 것은 +컴파일러에 물어본 답이다. ``` -uv run python tests/run.py 228/228 -uv run python tests/exec.py 32/32 +uv run python tests/run.py 245/245 + std 밖 unsafe/*T 예산 검사 +uv run python tests/exec.py 38/38 ``` -**검증 게이트: 매 항목마다 두 스위트 모두.** P0-1 과 P1-1 은 `run.py` 만으로는 -잡히지 않는다 -- 조용히 틀린 값을 내는 종류라 `exec.py` fixture 로 동작을 -고정해야 한다. - -**순서 근거:** P0 가 P3 를 막는다 (stdlib 표면 전체가 `?&T` 위에 얹힌다). -P1 은 P0-1 을 뺀 나머지 전부의 기준이다. P2 는 독립이라 언제 해도 된다. - --- -## P0 — 조용히 틀린 것 +## 끝난 것 -| # | 항목 | 무엇 | 왜 지금 | 규모 | -|---|---|---|---|---| -| 0-1 | **`?&T` lowering** | 옵셔널 참조를 니치로 표현한다 (참조는 null 이 될 수 없으므로 포인터 0 이 none). `== null`, `.?`, `if let`, `orelse` 전부 | `if let Some(r)` 이 **컴파일되고 쓰레기 값을 반환한다**. `== null` 은 `internal: cannot lower an aggregate as a value`. SPEC §5 R8 이 명시적으로 허용하는 타입인데 백엔드가 없다 | 중 | -| 0-2 | **제네릭 인스턴스 리터럴** | `Handle(Node){ raw: 7 }` 를 파싱한다 | 지금은 `Self{...}` 나 생성자 함수로만 인스턴스를 만들 수 있다. 타입 인자를 명시한 리터럴은 `expected ';'` 로 죽는다 | 소 | +### P0 — 조용히 틀린 것 -## P1 — 스펙의 빈칸 +| # | 무엇이었나 | | +|---|---|---| +| 0-1 | 니치 옵셔널(`?^T`, `?&T`)이 포인터가 아니라 포인터가 든 자리를 넘겼다. `if let` 이 컴파일되고 쓰레기를 반환했다 | `exec/optref.fe` | +| 0-2 | `Handle(Node){ raw: 7 }` 이 파싱되지 않았다. 제네릭 인스턴스는 `Self{...}` 나 생성자로만 지을 수 있었다 | `exec/geninst/` | -반나절짜리이고 이후 모든 결정의 기준이 된다. +### P1 — 스펙의 빈칸 -| # | 항목 | 결정 | 규모 | -|---|---|---|---| -| 1-1 | **정수 리터럴 기본 타입** | expected type 이 있으면 그 타입, 없으면 `i32`. 값이 대상 타입 범위를 벗어나면 컴파일 에러 (`let b: u8 = 300;`). `null` 이 이미 같은 방식이라 일관된다 | 중 | -| 1-2 | phantom 타입 파라미터 | §9 에 한 문장: "generic 타입 파라미터는 본문에서 사용되지 않아도 된다. `Handle(Node)` 와 `Handle(Type)` 은 서로 다른 nominal 인스턴스다." **구현은 이미 그렇게 동작한다** -- 미래의 구현자가 깨뜨리지 못하게 적어두는 것 | 소 | -| 1-3 | 오버플로 정의 | §7.4 에 "`--no-checks` 에서 오버플로 결과는 랩어라운드로 정의된다". i386 `add` 의 실제 동작이다 | 소 | -| 1-4 | 잔챙이 일곱 | 아래 표 | 중 | +| # | 결정 | | +|---|---|---| +| 1-1 | 정수 리터럴은 문맥이 요구하는 타입, 없으면 `i32`. 범위를 벗어나면 거부 | `types/badlit.fe`, `types/oklit.fe` | +| — | 그 자리에서 나온 것: **store 폭이 목적지가 아니라 값에서 왔다.** `let b: u8 = 200;` 이 4바이트를 1바이트 자리에 써서 옆 지역을 지웠다 | `exec/narrow.fe` | +| 1-2 | 미사용 타입 파라미터는 정상. typed handle 이 그 모양이다 | `generic/okphant.fe`, `badphant.fe` | +| 1-3 | `--no-checks` 에서 오버플로는 랩어라운드로 정의된다 | SPEC §7.4 | +| 1-4 | 작은 규칙 일곱 (SPEC §7.9). 다섯은 이미 옳았고, `defer` 안의 `return` 과 순회 중 원본 변경 둘은 그냥 통과하고 있었다 | `own/bdefret.fe`, `own/badforwr.fe`, `own/okforrd.fe` | -### 1-4 세부 +### P2 — SPEC 에서 죽은 C 백엔드 제거 -| 항목 | 결정 | -|---|---| -| 정수·char `match` arm | `_` 필수 | -| enum payload 에 `^T`/drop 있는 타입 | 허용. drop 은 활성 배리언트만 | -| `for x in slice` 순회 중 원본 | 순회 동안 원본 root 는 대여 상태 | -| `defer` 안에서 `return` | 컴파일 에러 | -| `undefined` 배열 | 슬라이스로 넘겨 채우는 것만 허용 | -| 외부 유닛에서 private 필드가 있는 struct 리터럴 | 컴파일 에러. 생성자 함수 강제 | -| by-value `self` 에서 부분 이동 | `mem.replace` 필요. 예외는 자기 `drop` 안뿐 (§5 R7) | +`.fei`, `--emit-c`, `fe_errors.h`, "호스트 C 방출", "C 방출 시 static inline" +열 군데. 감사가 그 문서를 충실히 읽고 존재하지 않는 문제(C 의 부호 있는 +오버플로 UB)를 보고했다 — 명세가 거짓말을 하면 그것을 읽는 사람이 틀린 답을 +낸다. 오류 코드 절은 실제대로 다시 썼고, `--target=`/`--model=` 은 드라이버에서 +없앴다. -## P2 — SPEC 에서 죽은 백엔드 제거 +### P3 — stdlib -C 백엔드는 없다. 파이프라인은 `.fe → i386 asm → wasm → wlink → .exe` 다. -그런데 `SPEC.md` 에 그 흔적이 열 군데 남아 있어서, 감사가 문서를 충실히 읽고 -존재하지 않는 문제(C 의 부호 있는 오버플로 UB)를 보고했다. +| # | | | +|---|---|---| +| 3-1 | `arena.Handle(T)` — 8바이트 고정. 세대·epoch·arena id 를 담아 놓아준 뒤, reset 뒤, 다른 아레나에 물으면 `null` | `exec/arenat.fe` | +| 3-2 | `arena.Arena(T)` — 짧은 대여만. `get` 은 사본을 준다. `get_mut` 은 두지 않았다 | | +| 3-3 | `list.List(T)` 에 `pop`/`take`/`swap`/`slice`/`slice_mut`/`clear` | `exec/listmor.fe` | +| 3-4 | `intern.Interner` — `str` 을 꺼내는 API 없음. `eq`/`len_of`/`hash_of`/`find`/`copy_into` | `exec/interns.fe` | +| 3-5 | `std.map` 이 `StrId` 를 바이트 키로 받는다. 별도 `IntMap` 불필요 | | -| # | 항목 | 무엇 | 규모 | -|---|---|---|---| -| 2-1 | C 백엔드 잔재 | `.fei`, `--emit-c`, `fe_errors.h`, "호스트 C 방출", "C 방출 시 static inline" 열 군데 | 소 | -| 2-2 | 오류 코드 절 재작성 | `.fei` 기반 증분 빌드 서술을 실제대로 -- 드라이버가 빌드 전체에서 모아 철자 순으로 1부터 | 소 | +길에서 고친 것 둘: +- 메서드 호출이 시그니처를 **호출자 유닛**에서 풀었다. +- 제네릭 인스턴스를 필드로 담은 구조체가 **크기 0 으로 굳었다** — 짓는 중인 + 인스턴스가 배치되어 버려서. `Holder` 가 36 바이트 대신 12 였다. -## P3 — stdlib +### P4 — 측정 -| # | 항목 | 표면 | 규모 | -|---|---|---|---| -| 3-1 | `Handle(T)` | **8바이트 고정.** `index 32 / slot_gen 16 / epoch 8 / arena_id 8`. `--no-checks` 는 **비교만 생략하고 레이아웃은 그대로** 둔다. 슬롯 gen 이 넘치면 그 슬롯은 영구 폐기 -- 랩어라운드로 stale 핸들이 되살아나는 것을 막는다 | 소 | -| 3-2 | `Arena(T)` | **짧은 대여만**: `alloc`, `get_copy`, `set`, `take`, `swap`, `free`, `reset`, `len`, `drop`. `reset` 은 슬롯별 gen 이 아니라 epoch 를 올린다 | 중 | -| 3-3 | `List(T)` 보강 | `pop`, `take`, `swap`, `slice`, `slice_mut` | 소 | -| 3-4 | `StringInterner` | `StrId{ raw: u32 }`. `intern`, `eq`, `eq_ids`, `hash`, `len`, `write`. **`str` 을 꺼내는 API 는 두지 않는다** -- 꺼내면 그 문자열이 사는 동안 interner 전체가 잠긴다 | 중 | -| 3-5 | `std.map` 판정 | 이미 `[]u8` 키라 별도 `IntMap` 이 필요 없다. `StrId` 를 4바이트 키로 쓰는지 확인만 | 소 | +`--report-unsafe`, `--report-instances`. 그리고 그 숫자를 `run.py` 가 검사한다: +`std.mem`/`std.sys` 밖의 `unsafe` 와 `*T` 는 0 이어야 하고, 늘어나면 빌드가 +실패한다. -## P4 — 측정 - -| # | 항목 | 무엇 | 규모 | -|---|---|---|---| -| 4-1 | `--report-unsafe` | 유닛별 `unsafe` 블록 수, `*T` 출현 수, `*_unchecked` 호출 수. 목표는 `std.mem`/`std.sys` 밖 0 개. CI 에서 회귀 검사 -- 늘어나면 실패 | 소 | -| 4-2 | `--report-instances` | 제네릭 인스턴스 수와 추정 크기 | 소 | +``` +unit unsafe *T unchecked +std.io 6 1 0 +std.sys 10 12 0 +total 16 13 0 +outside std 0 0 0 +``` --- -## 채택하지 않는 것 +## 채택하지 않은 것 | 감사 항목 | 판정 | 근거 | |---|---|---| -| 참조 튜플 `-> (&mut T, &mut T)` | **보류** | struct 는 필드 단위 대여로 이미 풀렸다 (`swap2(&mut p.a, &mut p.b)` 동작 확인). 컨테이너의 두 원소만 남는데 렉서·파서를 쓰면서 필요했던 자리가 0 번이다 | -| `Arena.get_mut -> ?&mut T` | **거부** | 감사 자신의 "참조를 오래 들고 있지 마라" 와 모순이다. 한 arena 안에서는 여전히 전체가 잠긴다 | -| `fmt.fmt_strid` | **거부** | `@print` 는 컴파일 단계에서 타입으로 `fmt_*` 를 고르는데, `StrId` 를 찍으려면 interner **인스턴스** 가 필요하고 R10 이 가변 전역 대여를 금지한다. `@print("{}", interner.text(id))` 로 간다 | -| 별도 `IntMap(V)` | **불필요** | `std.map` 이 `[]u8` 키라 이미 포괄한다 | -| C 오버플로 방출 규칙 | **대체** | C 백엔드가 없다. 결론(랩어라운드 정의)만 1-3 으로 흡수 | -| R4 완화 | **영구 제외** | 이걸 풀면 언어의 존재 이유가 없어진다 | +| 참조 튜플 `-> (&mut T, &mut T)` | 보류 | struct 는 필드 단위 대여로 풀렸다. 컨테이너의 두 원소만 남는데 렉서·파서에서 필요했던 자리가 0 번 | +| `Arena.get_mut -> ?&mut T` | 거부 | 아레나 하나를 통째로 잠그는 참조를 오래 들고 있게 하는 API | +| `fmt.fmt_strid` | 거부 | `@print` 가 interner 인스턴스에 닿을 수 없다 (R10) | +| 별도 `IntMap(V)` | 불필요 | `std.map` 이 이미 포괄 | +| C 오버플로 방출 규칙 | 대체 | C 백엔드가 없다. 결론만 1-3 으로 흡수 | +| R4 완화 | 영구 제외 | | -## 이미 끝난 것 +--- -| 감사 항목 | 상태 | -|---|---| -| depth-1 field-sensitive 대여 | `f7e6676`. `std.map` 의 `keep` 이 다시 함수 하나가 됐다 | -| Copy AST 노드 | `ast.Node` 는 정수와 핸들뿐이라 자연히 Copy | -| 같은 struct 의 두 `&mut` | 필드 단위 대여로 풀림 | -| phantom 파라미터 동작 | 구현은 이미 지원. 문장만 P1-2 | -| Ferro 렉서·파서를 Ferro 로 | `fec/tests/exec/lexer/` | -| R4, R10, R9, 블록 표현식 배제, 오류 번호표, Copy handle enum | 손대지 않는다 | +## 다음 + +부트스트랩까지 남은 것은 `TODO.md` 에 있다. 도구는 다 갖췄다 — 아레나, 핸들, +맵, interner, 그리고 리졸버가 쓸 `Node.bind` 와 `Map.clear`. From 52aaff62e490e37a0995aaaf7cbda47cf98e54a7 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:34:15 +0900 Subject: [PATCH 180/184] =?UTF-8?q?docs:=20TODO=20=EB=A5=BC=20=EC=A7=80?= =?UTF-8?q?=EA=B8=88=20=EC=83=81=ED=83=9C=EB=A1=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 셀프호스팅 목록을 다시 매겼다. 다음은 리졸버를 Ferro 로이고 도구는 다 있다: intern, map, Node.bind, Map.clear. 정해진 것에 셋을 더했다: 정수 리터럴의 타입, store 폭은 값이 아니라 자리가 정한다는 것, std 밖 unsafe 예산이 0 이며 run.py 가 검사한다는 것. 남은 구멍에 둘: ?^T 의 자동 해제가 아직 없고, 컨테이너 두 원소의 동시 &mut 는 인덱스가 갈라지지 않아 stdlib 안에서 푼다. 245/245, 38/38. --- TODO.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/TODO.md b/TODO.md index d00362f..d14139a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,8 +1,8 @@ # TODO ``` -uv run python tests/run.py 224/224 컴파일러가 프로그램에 대해 뭐라고 하는가 -uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 무엇을 하는가 +uv run python tests/run.py 245/245 컴파일러가 프로그램에 대해 뭐라고 하는가 +uv run python tests/exec.py 38/38 컴파일된 프로그램이 실제로 무엇을 하는가 ``` ``` @@ -25,6 +25,7 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | `std.map` | 키를 맵이 소유한 버퍼에 복사하고 슬롯은 위치만 든다 | | **Ferro 파서를 Ferro 로** | 노드 배열 하나 + 인덱스. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 묶인다 | | 필드 단위 대여 | `p.a` 와 `p.b` 는 다른 place 다. `std.map` 의 `keep` 이 다시 함수 하나가 됐다 | +| **GOAL.md P0~P4** | 외부 감사에서 나온 것 전부. stdlib 다섯, 스펙 빈칸, 죽은 C 백엔드, 측정 | 파서가 알려준 것: **자기 참조 자료구조는 인덱스로 짓는다.** 노드는 `^Node` 를 들 수 없고(자식이 여럿이며 한 번씩 소유하지 않는다) `&Node` 도 들 수 없다(R4). @@ -38,10 +39,11 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | # | 일 | 규모 | 비고 | |---|---|---|---| -| 1 | 심볼 표와 이름 해석을 Ferro 로 | 중 | `std.map` 이 준비됐다 | -| 2 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | -| 3 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | -| 4 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | +| 1 | 리졸버를 Ferro 로 | 중 | 도구는 다 있다: `intern`, `map`, `Node.bind`, `Map.clear` | +| 2 | 타입 검사를 Ferro 로 | 대 | 리졸버 다음 | +| 3 | `io.read` 로 줄 단위 읽기 | 소 | 지금은 버퍼 하나로 통째로 읽는다 | +| 4 | 여러 반환값 또는 out 파라미터 | 중 | `&mut` 재대여로 되지만 장황하다 | +| 5 | `fec` 을 Ferro 로 | 대 | 여기까지 오면 언어가 자기 무게를 견딘다 | ## 언어에 남은 구멍 @@ -52,7 +54,8 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | `@sprint` | 전개하지 않는다. `@print`/`@fprint` 만 | | `interrupt` `shared` `atomic` `critical` | 파싱만 되고 의미 없음. SPEC §11 에서 v0.2 | | lowering 미구현 진단 | `internal: cannot lower X`. 사용자 오류처럼 보이지 않는다 | -| 배열·enum 페이로드의 자동 해제 | `release_at` 은 구조체 필드까지만 내려간다 | +| 배열·enum 페이로드의 자동 해제 | `release_at` 은 구조체 필드까지만 내려간다. `?^T` 도 아직이라 `mem.replace` 로 직접 꺼내야 한다 | +| 컨테이너 두 원소의 동시 `&mut` | 인덱스는 갈라지지 않는다. `swap` 같은 것은 stdlib 안에서 해결한다 | | `--strip-error-names` | 받아들이지만 아무것도 하지 않는다 (SPEC §4.6) | | `fmt.fmt_error` | 없다. SPEC §4.6 이 약속만 하고 있다 | | `0b` / `0o` 리터럴 | 렉서는 받지만 값 계산이 10진과 16진만 안다 | @@ -71,6 +74,9 @@ uv run python tests/exec.py 31/31 컴파일된 프로그램이 실제로 | 슬라이스 배치 | 포인터 다음 길이. 오프셋은 `lowerpri.h` 한 군데에만 | | 오류 코드 | `error.Name` 을 빌드 전체에서 모아 철자 순으로 1부터 | | 레지스터 | ebx·esi·edi 를 블록 안에 머무는 임시값에 준다. eax/ecx/edx 는 스크래치 | +| 정수 리터럴 | 문맥이 요구하는 타입, 없으면 `i32`. 범위를 벗어나면 거부 (SPEC §3) | +| store 폭 | 값이 아니라 **자리**가 정한다 | +| unsafe 예산 | `std.mem`/`std.sys` 밖은 0. `run.py` 가 검사하고 늘어나면 실패한다 | | 해제 | 소유자를 놓으면 그것이 가진 것도 놓는다 (R1). `drop` 을 부른 뒤 필드로 내려간다 — 그래서 `List`/`Arena`/`Map` 은 `drop` 이 없다 | | 배타 대여를 호출에 넘기기 | **이동이 아니라 호출 동안의 재대여** (SPEC §4.2, §5 R6). 없으면 배타 파라미터를 함수당 한 번만 넘길 수 있어 재귀 하강 파서를 못 쓴다 | | 자기 `drop` 안의 부분 이동 | **허용** (SPEC §5 R7 예외). 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다 | From 6dc298d828872409fdf6b7d2e85830f18a118d9f Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:40:50 +0900 Subject: [PATCH 181/184] =?UTF-8?q?docs:=20=EB=AC=B8=EC=84=9C=20=ED=95=98?= =?UTF-8?q?=EB=82=98=EA=B0=80=20=EC=A7=88=EB=AC=B8=20=ED=95=98=EB=82=98?= =?UTF-8?q?=EC=97=90=20=EB=8B=B5=ED=95=98=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GOAL.md 는 계획일 때 값어치가 있었는데 완료 보고로 바꾸는 순간 git log 의 중복이 됐다. 끝난 계획은 문서가 아니라 히스토리다. 내용은 이미 TODO 와 SPEC 에 들어갔으니 지운다. fixture-report.md 도 지운다. 기준선이 150/188 인 일회성 조사 스냅샷이고 지금은 245/245 다. 필요하면 2f140e1 에 있다. TODO 의 '정해진 것' 이 두 종류를 섞고 있었다. 언어 규칙은 SPEC 문장을 옮겨 적은 것이라 한쪽만 고치면 갈라지고, 구현 결정은 SPEC 에 없어서 여기가 유일한 출처다. 둘로 쪼갰다 -- 앞의 표는 §번호만 담고, 뒤의 표는 내용과 그것이 사는 파일을 담는다. SPEC 의 §7.9 를 §7.7 로 옮겼다. 7.7 과 7.8 없이 7.9 가 떠 있었다. 1847 → 1173 줄. 245/245, 38/38. --- AGENTS.md | 3 +- GOAL.md | 88 ---- SPEC.md | 2 +- TODO.md | 52 ++- audits/2026-08-17-spec-fec-parser.md | 25 ++ fixture-report.md | 597 --------------------------- 6 files changed, 59 insertions(+), 708 deletions(-) delete mode 100644 GOAL.md create mode 100644 audits/2026-08-17-spec-fec-parser.md delete mode 100644 fixture-report.md diff --git a/AGENTS.md b/AGENTS.md index bc8dcf9..f881759 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,7 @@ DOS/Windows용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. |---|---| | `SPEC.md` | 언어 명세. 유일한 규범 문서 | | `IR.md` | 중간 표현. 프론트엔드와 기계 사이 | -| `TODO.md` | 남은 작업, 미결 결정, 정해진 것 | -| `GOAL.md` | 부트스트랩까지의 실행 계획. P0~P4 와 안 하기로 한 것 | +| `TODO.md` | 남은 작업과 정해진 것. 언어 규칙은 `SPEC.md` 를 가리키기만 한다 | | `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 | ## 파이프라인 diff --git a/GOAL.md b/GOAL.md deleted file mode 100644 index cbaad00..0000000 --- a/GOAL.md +++ /dev/null @@ -1,88 +0,0 @@ -# GOAL — 부트스트랩까지 남은 작업 - -외부 스펙 감사(v0.1.8)와, 그 항목들을 실제로 빌드해서 확인한 결과를 합친 실행 -계획이었다. **P0~P4 는 전부 끝났다.** 감사는 문서를 읽었고 여기 적힌 것은 -컴파일러에 물어본 답이다. - -``` -uv run python tests/run.py 245/245 + std 밖 unsafe/*T 예산 검사 -uv run python tests/exec.py 38/38 -``` - ---- - -## 끝난 것 - -### P0 — 조용히 틀린 것 - -| # | 무엇이었나 | | -|---|---|---| -| 0-1 | 니치 옵셔널(`?^T`, `?&T`)이 포인터가 아니라 포인터가 든 자리를 넘겼다. `if let` 이 컴파일되고 쓰레기를 반환했다 | `exec/optref.fe` | -| 0-2 | `Handle(Node){ raw: 7 }` 이 파싱되지 않았다. 제네릭 인스턴스는 `Self{...}` 나 생성자로만 지을 수 있었다 | `exec/geninst/` | - -### P1 — 스펙의 빈칸 - -| # | 결정 | | -|---|---|---| -| 1-1 | 정수 리터럴은 문맥이 요구하는 타입, 없으면 `i32`. 범위를 벗어나면 거부 | `types/badlit.fe`, `types/oklit.fe` | -| — | 그 자리에서 나온 것: **store 폭이 목적지가 아니라 값에서 왔다.** `let b: u8 = 200;` 이 4바이트를 1바이트 자리에 써서 옆 지역을 지웠다 | `exec/narrow.fe` | -| 1-2 | 미사용 타입 파라미터는 정상. typed handle 이 그 모양이다 | `generic/okphant.fe`, `badphant.fe` | -| 1-3 | `--no-checks` 에서 오버플로는 랩어라운드로 정의된다 | SPEC §7.4 | -| 1-4 | 작은 규칙 일곱 (SPEC §7.9). 다섯은 이미 옳았고, `defer` 안의 `return` 과 순회 중 원본 변경 둘은 그냥 통과하고 있었다 | `own/bdefret.fe`, `own/badforwr.fe`, `own/okforrd.fe` | - -### P2 — SPEC 에서 죽은 C 백엔드 제거 - -`.fei`, `--emit-c`, `fe_errors.h`, "호스트 C 방출", "C 방출 시 static inline" -열 군데. 감사가 그 문서를 충실히 읽고 존재하지 않는 문제(C 의 부호 있는 -오버플로 UB)를 보고했다 — 명세가 거짓말을 하면 그것을 읽는 사람이 틀린 답을 -낸다. 오류 코드 절은 실제대로 다시 썼고, `--target=`/`--model=` 은 드라이버에서 -없앴다. - -### P3 — stdlib - -| # | | | -|---|---|---| -| 3-1 | `arena.Handle(T)` — 8바이트 고정. 세대·epoch·arena id 를 담아 놓아준 뒤, reset 뒤, 다른 아레나에 물으면 `null` | `exec/arenat.fe` | -| 3-2 | `arena.Arena(T)` — 짧은 대여만. `get` 은 사본을 준다. `get_mut` 은 두지 않았다 | | -| 3-3 | `list.List(T)` 에 `pop`/`take`/`swap`/`slice`/`slice_mut`/`clear` | `exec/listmor.fe` | -| 3-4 | `intern.Interner` — `str` 을 꺼내는 API 없음. `eq`/`len_of`/`hash_of`/`find`/`copy_into` | `exec/interns.fe` | -| 3-5 | `std.map` 이 `StrId` 를 바이트 키로 받는다. 별도 `IntMap` 불필요 | | - -길에서 고친 것 둘: -- 메서드 호출이 시그니처를 **호출자 유닛**에서 풀었다. -- 제네릭 인스턴스를 필드로 담은 구조체가 **크기 0 으로 굳었다** — 짓는 중인 - 인스턴스가 배치되어 버려서. `Holder` 가 36 바이트 대신 12 였다. - -### P4 — 측정 - -`--report-unsafe`, `--report-instances`. 그리고 그 숫자를 `run.py` 가 검사한다: -`std.mem`/`std.sys` 밖의 `unsafe` 와 `*T` 는 0 이어야 하고, 늘어나면 빌드가 -실패한다. - -``` -unit unsafe *T unchecked -std.io 6 1 0 -std.sys 10 12 0 -total 16 13 0 -outside std 0 0 0 -``` - ---- - -## 채택하지 않은 것 - -| 감사 항목 | 판정 | 근거 | -|---|---|---| -| 참조 튜플 `-> (&mut T, &mut T)` | 보류 | struct 는 필드 단위 대여로 풀렸다. 컨테이너의 두 원소만 남는데 렉서·파서에서 필요했던 자리가 0 번 | -| `Arena.get_mut -> ?&mut T` | 거부 | 아레나 하나를 통째로 잠그는 참조를 오래 들고 있게 하는 API | -| `fmt.fmt_strid` | 거부 | `@print` 가 interner 인스턴스에 닿을 수 없다 (R10) | -| 별도 `IntMap(V)` | 불필요 | `std.map` 이 이미 포괄 | -| C 오버플로 방출 규칙 | 대체 | C 백엔드가 없다. 결론만 1-3 으로 흡수 | -| R4 완화 | 영구 제외 | | - ---- - -## 다음 - -부트스트랩까지 남은 것은 `TODO.md` 에 있다. 도구는 다 갖췄다 — 아레나, 핸들, -맵, interner, 그리고 리졸버가 쓸 `Node.bind` 와 `Map.clear`. diff --git a/SPEC.md b/SPEC.md index c6a1a9a..13b0147 100644 --- a/SPEC.md +++ b/SPEC.md @@ -581,7 +581,7 @@ pub fn main() -> !void { --- -### 7.9 작은 규칙들 +### 7.7 작은 규칙들 구현자가 임의로 정하면 갈라지는 것들. 각각 한 줄이면 끝나므로 여기 모아 둔다. diff --git a/TODO.md b/TODO.md index d14139a..0db2f06 100644 --- a/TODO.md +++ b/TODO.md @@ -25,7 +25,7 @@ uv run python tests/exec.py 38/38 컴파일된 프로그램이 실제로 | `std.map` | 키를 맵이 소유한 버퍼에 복사하고 슬롯은 위치만 든다 | | **Ferro 파서를 Ferro 로** | 노드 배열 하나 + 인덱스. `1 + 2 * 3` 이 `(+ 1 (* 2 3))` 로 묶인다 | | 필드 단위 대여 | `p.a` 와 `p.b` 는 다른 place 다. `std.map` 의 `keep` 이 다시 함수 하나가 됐다 | -| **GOAL.md P0~P4** | 외부 감사에서 나온 것 전부. stdlib 다섯, 스펙 빈칸, 죽은 C 백엔드, 측정 | +| 외부 스펙 감사 대응 | 니치 옵셔널·store 폭·배치 세 버그, 스펙 빈칸, 죽은 C 백엔드, stdlib 다섯, 측정 | 파서가 알려준 것: **자기 참조 자료구조는 인덱스로 짓는다.** 노드는 `^Node` 를 들 수 없고(자식이 여럿이며 한 번씩 소유하지 않는다) `&Node` 도 들 수 없다(R4). @@ -35,8 +35,6 @@ uv run python tests/exec.py 38/38 컴파일된 프로그램이 실제로 ## 셀프호스팅으로 가는 길 -순서와 근거는 `GOAL.md` 에 있다 (P0~P4). 여기는 무엇이 남았는지만 적는다. - | # | 일 | 규모 | 비고 | |---|---|---|---| | 1 | 리졸버를 Ferro 로 | 중 | 도구는 다 있다: `intern`, `map`, `Node.bind`, `Map.clear` | @@ -62,29 +60,43 @@ uv run python tests/exec.py 38/38 컴파일된 프로그램이 실제로 --- -## 정해진 것 +## 정해진 것 — 언어 + +`SPEC.md` 가 유일한 출처다. 여기는 **어디를 보는지만** 적는다. 문장을 옮겨 +적으면 한쪽만 고쳐져서 갈라진다. | | | |---|---| -| 타깃 | **i386 하나.** 세그먼트 없음, `far` 영구 제외 (SPEC §2) | -| `usize`/`isize` | **타깃의 포인터 폭.** 비트 수를 약속하지 않아 64비트 문이 닫히지 않음 | -| 제네릭 | 모노모피제이션. 순수 프론트엔드 기능이라 IR 에 제네릭 개념이 없음 | -| 덩어리 전달 | 전부 주소로. ISA 마다 다른 구조체 전달 ABI 를 피해감 | -| 트랩 | `trap ` → `fe_trap(reason, FE_FILE_n, line)`. 파일은 검사가 쓰인 유닛 | -| 슬라이스 배치 | 포인터 다음 길이. 오프셋은 `lowerpri.h` 한 군데에만 | -| 오류 코드 | `error.Name` 을 빌드 전체에서 모아 철자 순으로 1부터 | -| 레지스터 | ebx·esi·edi 를 블록 안에 머무는 임시값에 준다. eax/ecx/edx 는 스크래치 | -| 정수 리터럴 | 문맥이 요구하는 타입, 없으면 `i32`. 범위를 벗어나면 거부 (SPEC §3) | -| store 폭 | 값이 아니라 **자리**가 정한다 | -| unsafe 예산 | `std.mem`/`std.sys` 밖은 0. `run.py` 가 검사하고 늘어나면 실패한다 | -| 해제 | 소유자를 놓으면 그것이 가진 것도 놓는다 (R1). `drop` 을 부른 뒤 필드로 내려간다 — 그래서 `List`/`Arena`/`Map` 은 `drop` 이 없다 | -| 배타 대여를 호출에 넘기기 | **이동이 아니라 호출 동안의 재대여** (SPEC §4.2, §5 R6). 없으면 배타 파라미터를 함수당 한 번만 넘길 수 있어 재귀 하강 파서를 못 쓴다 | -| 자기 `drop` 안의 부분 이동 | **허용** (SPEC §5 R7 예외). 객체가 사라지는 중이라 뒤에 남아 읽힐 것이 없다 | -| 대여 단위 | **place 단위** (SPEC §5 R6). 루트 변수의 직속 필드 한 겹까지 갈라지고, 그 아래와 인덱스·역참조는 전체 대여다. 한 값당 4 필드까지, 넘으면 전체로 되돌아간다 | +| 타깃, `usize`/`isize` | §2 | +| 정수 리터럴의 타입과 범위 | §3 | +| 호출 인자·반환 위치의 대여 약화 | §4.2 | +| 해제는 소유자를 따라 내려간다 | §5 R1 | +| 대여는 place 단위 | §5 R6 | +| 배타 대여를 호출에 넘기는 것은 재대여 | §4.2, §5 R6 | +| 자기 `drop` 안의 부분 이동 | §5 R7 | +| `--no-checks` 의 오버플로는 랩어라운드 | §7.4 | +| `match`·`defer`·순회·`undefined` 등 작은 규칙 일곱 | §7.7 | +| 제네릭은 모노모피제이션 | §9 | + +## 정해진 것 — 구현 + +`SPEC.md` 에 없는 것들. **여기가 출처다.** + +| | | 사는 곳 | +|---|---|---| +| 덩어리 전달 | 전부 주소로. ISA 마다 다른 구조체 전달 ABI 를 피해감 | `lower.c` | +| 슬라이스 배치 | 포인터 다음 길이 | `lowerpri.h` 한 군데에만 | +| store 폭 | 값이 아니라 **자리**가 정한다 | `lowerstm.c` `store_into` | +| 트랩 | `fe_trap(reason, FE_FILE_n, line)`. 파일은 검사가 쓰인 유닛 | `x86.c`, `rt/start.asm` | +| 오류 코드 | 빌드 전체에서 모아 철자 순으로 1부터 | `lowerstm.c` | +| 레지스터 | ebx·esi·edi 를 블록 안에 머무는 임시값에. eax/ecx/edx 는 스크래치 | `x86.c` | +| 제네릭 인스턴스 | 짓는 중에는 배치하지 않는다. 멤버가 안 정해진 집합 타입은 굳지 않고 물러난다 | `types.c` `layout_type` | +| unsafe 예산 | `std.mem`/`std.sys` 밖은 0. 늘어나면 빌드가 실패한다 | `tests/run.py` | --- ## 파일 크기 규칙 **2,000 줄을 넘기지 않는다. 웬만하면 1,000 줄.** 넘으면 나눈다 -- `check.c` 는 -3,937 줄이었고 `lower.c` 는 1,913 줄이었다. 지금 가장 큰 것은 1,000 줄이다. +3,937 줄이었고 `lower.c` 는 1,913 줄이었다. 지금 가장 큰 것은 `checkcal.c` +1,044 줄이다. diff --git a/audits/2026-08-17-spec-fec-parser.md b/audits/2026-08-17-spec-fec-parser.md new file mode 100644 index 0000000..0a406b4 --- /dev/null +++ b/audits/2026-08-17-spec-fec-parser.md @@ -0,0 +1,25 @@ +# SPEC–fec parser audit + +- 날짜: 2026-08-17 +- 기준 커밋: `52aaff62e490e37a0995aaaf7cbda47cf98e54a7` +- 범위: `SPEC.md` §6과 `fec/src/lexer.c`, `fec/src/parser.c` + +## 현재 문제 + +| ID | SPEC | 현재 구현 | 재현 결과 | +|---|---|---|---| +| PARSE-01 | 최상위 `comptime if` 선언 허용 | `comptime if`는 문장에서만 처리 | 최상위 사용을 `expected declaration`으로 거부 | +| PARSE-02 | 타입 이름은 `ident ('.' ident)*` | 타입에서 점 하나만 처리 | `alpha.beta.Gamma`를 파싱하지 못함 | +| PARSE-03 | `catch` EBNF가 binding 없는 block과 binding 뒤 expression도 허용 | 짧은 `catch expr`과 `catch \|e\| block`만 처리 | 구현은 §4.6 설명과 맞고 §6.1 EBNF가 지나치게 넓음 | +| PARSE-04 | `\|`와 `^`는 같은 우선순위, 단항 비트 NOT은 `~` | 각각 우선순위 5와 6, `~` 토큰 없음, 단항 `^` 허용 | `~x`를 거부하고 `a \| b ^ c`를 `a \| (b ^ c)`로 파싱 | +| PARSE-05 | 전역 `static`과 `var`의 타입 필수 | 타입 표기를 선택적으로 처리하고 초기값에서 추론 | `static A = 1;`, `var B = 2;` 모두 검사 통과 | +| PARSE-06 | error code는 정수 literal | 일반 expression을 파싱하며 literal이 아니면 code 검증을 건너뜀 | `error E { Bad = 1 + 2, }`가 검사 통과 | +| PARSE-07 | struct field와 enum vfield의 쉼표 필수 | 닫는 `}` 바로 앞에서는 쉼표 생략 허용 | `struct S { x: i32 }`가 검사 통과 | + +## 검증 + +- `uv run python tests/run.py`: `240/240` 통과 +- 위 항목의 최소 입력을 현재 `fec`에 직접 넣어 파싱 및 `--check` 결과를 확인함 +- 기존 fixture에는 위 괴리를 직접 고정하는 사례가 없음 + +이 문서는 조사 시점의 구현 상태를 기록한다. 언어 규칙의 기준은 `SPEC.md`다. diff --git a/fixture-report.md b/fixture-report.md deleted file mode 100644 index 0bec0ce..0000000 --- a/fixture-report.md +++ /dev/null @@ -1,597 +0,0 @@ -# 마커 없는 fixture 진단 증거 보고서 - -기준선: `uv run python tests/run.py` → `150/188 passed (58 pin a line and message)` - -이 보고서는 마커가 없는 37개 fixture를 직접 읽고, 기준선에서 새로 빌드된 -`.build/fec.exe --check`의 실제 진단을 확인한 결과다. 진단 전문의 색상 제어 문자는 -가독성을 위해 제거했으며, 텍스트·줄·열·진단 순서는 그대로 기록했다. - -## types - -### `fec/tests/types/bad_ari.fe` - -- 검사 대상: 함수 호출 인자 개수 불일치. -- 근거: `add`는 두 인자를 받지만 8행에서 `add(1)`로 한 인자만 전달한다. -- 실제 진단: - - ```text - fec/tests/types/bad_ari.fe:8:15: error: wrong number of arguments - 8 | return add(1); - | ^ - ``` - -- 일치 여부: 예 — 호출의 인자 개수 위반을 직접 진단한다. - -### `fec/tests/types/bad_asgn.fe` - -- 검사 대상: 불변 `let` 변수에 대입. -- 근거: 4행에서 `let value`로 선언한 뒤 5행에서 `value = 2`로 대입한다. -- 실제 진단: - - ```text - fec/tests/types/bad_asgn.fe:5:5: error: cannot assign to immutable let - 5 | value = 2; - | ^ - ``` - -- 일치 여부: 예 — `let`의 불변성 위반을 직접 진단한다. - -### `fec/tests/types/bad_cast.fe` - -- 검사 대상: 허용되지 않는 타입의 `as` 변환. -- 근거: 4행에서 `bool` 값 `true`를 `i32`로 변환한다. -- 실제 진단: - - ```text - fec/tests/types/bad_cast.fe:4:23: error: 'as' requires integer or char types - 4 | let x: i32 = true as i32; - | ^ - ``` - -- 일치 여부: 예 — 정수/문자가 아닌 피연산자의 캐스트를 직접 진단한다. - -### `fec/tests/types/bad_cond.fe` - -- 검사 대상: 조건식의 비-`bool` 값 사용. -- 근거: 4행의 `if 1`에서 정수 리터럴을 조건으로 사용한다. -- 실제 진단: - - ```text - fec/tests/types/bad_cond.fe:4:5: error: if condition must be bool - 4 | if 1 { return 0; } - | ^ - ``` - -- 일치 여부: 예 — 조건식이 `bool`이어야 한다는 규칙을 직접 진단한다. - -### `fec/tests/types/bad_mlet.fe` - -- 검사 대상: `let`으로 mutable slice를 바인딩. -- 근거: 4행에서 mutable 배열 slice를 만든 뒤 5행의 `let s: []mut u8`에 바인딩한다. -- 실제 진단: - - ```text - fec/tests/types/bad_mlet.fe:5:5: error: let cannot bind a mutable slice - 5 | let s: []mut u8 = raw[..]; - | ^ - ``` - -- 일치 여부: 예 — mutable slice의 `let` 바인딩 금지를 직접 진단한다. - -### `fec/tests/types/bad_ret.fe` - -- 검사 대상: 반환식과 함수 반환 타입의 불일치. -- 근거: `main`은 `i32`를 반환한다고 선언했지만 4행에서 `true`를 반환한다. -- 실제 진단: - - ```text - fec/tests/types/bad_ret.fe:4:5: error: return type mismatch - 4 | return true; - | ^ - ``` - -- 일치 여부: 예 — 반환 타입 불일치를 직접 진단한다. - -### `fec/tests/types/bad_shwr.fe` - -- 검사 대상: shared slice를 통한 쓰기. -- 근거: `s`는 `[]u8` shared slice인데 4행에서 `s[0] = 1`로 쓴다. -- 실제 진단: - - ```text - fec/tests/types/bad_shwr.fe:4:6: error: cannot write through shared slice - 4 | s[0] = 1; - | ^ - ``` - -- 일치 여부: 예 — shared slice 쓰기 위반을 직접 진단한다. - -### `fec/tests/types/bad_type.fe` - -- 검사 대상: 함수 인자 타입 불일치. -- 근거: `add`의 첫 인자는 `i32`인데 8행에서 `true`를 전달한다. -- 실제 진단: - - ```text - fec/tests/types/bad_type.fe:8:16: error: argument type mismatch - 8 | return add(true, 1); - | ^ - ``` - -- 일치 여부: 예 — 함수 인자의 타입 불일치를 직접 진단한다. - -### `fec/tests/types/bad_unit.fe` - -- 검사 대상: 초기화되지 않은 지역 변수 사용. -- 근거: 4행에서 `var value: i32`만 선언하고 값을 넣지 않은 채 5행에서 반환한다. -- 실제 진단: - - ```text - fec/tests/types/bad_unit.fe:5:12: error: use of uninitialized variable - 5 | return value; - | ^ - ``` - -- 일치 여부: 예 — 초기화되지 않은 변수 사용을 직접 진단한다. - -### `fec/tests/types/bad_unk.fe` - -- 검사 대상: 정의되지 않은 이름 사용. -- 근거: 4행에서 선언되지 않은 `missing_name`을 반환한다. -- 실제 진단: - - ```text - fec/tests/types/bad_unk.fe:4:12: error: unknown name - 4 | return missing_name; - | ^ - ``` - -- 일치 여부: 예 — 미정의 이름 사용을 직접 진단한다. - -### `fec/tests/types/bad_void.fe` - -- 검사 대상: `void` 표현식을 값 변수의 초기화식으로 사용. -- 근거: 반환값이 없는 `noop()`의 결과를 8행에서 `i32` 변수에 넣는다. -- 실제 진단: - - ```text - fec/tests/types/bad_void.fe:8:5: error: initializer type mismatch - 8 | let value: i32 = noop(); - | ^ - fec/tests/types/bad_void.fe:8:5: error: void expression cannot initialize a variable - 8 | let value: i32 = noop(); - | ^ - ``` - -- 일치 여부: 예 — void 표현식의 값 초기화 사용을 직접 진단한다. - -### `fec/tests/types/badarr.fe` - -- 검사 대상: 배열 리터럴의 원소 타입 및 선언된 배열 타입 불일치. -- 근거: `[2]i32`에 세 원소를 쓰고, 둘째 원소로 `bool`인 `true`를 넣는다. -- 실제 진단: - - ```text - fec/tests/types/badarr.fe:3:25: error: array element type mismatch - 3 | let a: [2]i32 = [1, true, 3]; - | ^ - fec/tests/types/badarr.fe:3:5: error: initializer type mismatch - 3 | let a: [2]i32 = [1, true, 3]; - | ^ - ``` - -- 일치 여부: 예 — 배열 원소 타입 위반을 직접 진단하고, 선언 타입 불일치도 함께 진단한다. - -### `fec/tests/types/badchar.fe` - -- 검사 대상: 명시적 캐스트 없는 `char`와 `u8`의 대입. -- 근거: 4행에서 `char` 리터럴을 `u8` 변수에 직접 넣는다. -- 실제 진단: - - ```text - fec/tests/types/badchar.fe:4:5: error: initializer type mismatch - 4 | let u: u8 = 'A'; - | ^ - fec/tests/types/badchar.fe:5:5: error: return type mismatch - 5 | return u; - | ^ - ``` - -- 일치 여부: 예 — 첫 진단이 char/u8 직접 대입의 타입 불일치를 짚는다. 5행 진단은 연쇄 오류다. - -### `fec/tests/types/badcycle.fe` - -- 검사 대상: 값으로 연결된 재귀 구조체 타입. -- 근거: `A`가 `B`를 값으로 포함하고 `B`가 다시 `A`를 값으로 포함한다. -- 실제 진단: - - ```text - fec/tests/types/badcycle.fe:1:1: error: by-value recursive type - 1 | unit badcycle; - | ^ - ``` - -- 일치 여부: 예 — 값 기반 재귀 타입을 직접 진단한다. 위치가 선언부 첫 줄로 올라가지만 진단 종류는 정확하다. - -### `fec/tests/types/badfield.fe` - -- 검사 대상: 불변 구조체 값의 필드에 대입. -- 근거: `p`는 `let`으로 선언됐고 6행에서 `p.x = 3`을 수행한다. -- 실제 진단: - - ```text - fec/tests/types/badfield.fe:6:6: error: cannot assign through immutable value - 6 | p.x = 3; - | ^ - ``` - -- 일치 여부: 예 — 불변 값의 projection을 통한 쓰기를 직접 진단한다. - -### `fec/tests/types/badfld.fe` - -- 검사 대상: 구조체 초기화 필드 누락과 존재하지 않는 필드 접근. -- 근거: `Point`는 `x`, `y`를 요구하지만 4행 초기화에는 `x`만 있고, 5행에서 없는 `z`에 접근한다. -- 실제 진단: - - ```text - fec/tests/types/badfld.fe:4:20: error: missing struct field - 4 | let p: Point = Point{ x: 1 }; - | ^ - fec/tests/types/badfld.fe:5:13: error: unknown struct field - 5 | return p.z; - | ^ - ``` - -- 일치 여부: 예 — 두 필드 규칙 위반을 모두 직접 진단한다. - -### `fec/tests/types/badindex.fe` - -- 검사 대상: 불변 배열을 통한 요소 쓰기. -- 근거: `a`는 `let` 배열인데 5행에서 `a[0] = 3`을 수행한다. -- 실제 진단: - - ```text - fec/tests/types/badindex.fe:5:6: error: cannot assign through immutable value - 5 | a[0] = 3; - | ^ - ``` - -- 일치 여부: 예 — 불변 배열 index projection을 통한 쓰기를 직접 진단한다. - -### `fec/tests/types/badmat.fe` - -- 검사 대상: 비-완전 `match`. -- 근거: `Shape`에는 `Empty`, `Circle` 두 variant가 있는데 4행 match에는 `Empty`만 있다. -- 실제 진단: - - ```text - fec/tests/types/badmat.fe:4:5: error: non-exhaustive match - 4 | match Shape.Empty { Empty => 0; } - | ^ - ``` - -- 일치 여부: 예 — match의 비-완전성을 직접 진단한다. - -### `fec/tests/types/badstr.fe` - -- 검사 대상: shared string/slice를 통한 쓰기. -- 근거: `str`인 `text`의 4행에서 인덱스 요소에 대입한다. -- 실제 진단: - - ```text - fec/tests/types/badstr.fe:4:9: error: cannot write through shared slice - 4 | text[0] = 'z'; - | ^ - fec/tests/types/badstr.fe:4:13: error: assignment type mismatch - 4 | text[0] = 'z'; - | ^ - ``` - -- 일치 여부: 예 — 첫 진단이 shared string 쓰기를 직접 짚고, 두 번째는 요소 타입의 연쇄 진단이다. - -## format - -### `fec/tests/format/bad_ari.fe` - -- 검사 대상: format placeholder와 인자 개수 불일치. -- 근거: 4행의 format 문자열에는 `{}`가 두 개지만 인자는 `1` 하나다. -- 실제 진단: - - ```text - fec/tests/format/bad_ari.fe:4:6: error: format argument count mismatch - 4 | @print("{} {}", 1); - | ^ - fec/tests/format/bad_ari.fe:4:6: error: format argument count mismatch - 4 | @print("{} {}", 1); - | ^ - ``` - -- 일치 여부: 예 — 인자 개수 불일치를 직접 진단한다. 동일 진단이 중복 출력된다. - -### `fec/tests/format/bad_bufw.fe` - -- 검사 대상: `io.buf_writer(buf)`를 통한 buffer writer 구성. -- 근거: `[]mut u8` 버퍼를 `io.buf_writer`에 전달하지만, 이 호출이 정확히 어떤 금지 규칙을 의도하는지는 파일만으로 확정하기 어렵다. 명세의 `io.Writer`는 enum handle이며 `io.buf_writer` API는 정의되어 있지 않다. -- 실제 진단: - - ```text - fec/tests/format/bad_bufw.fe:6:13: error: unknown name - 6 | let w = io.buf_writer(buf); - | ^ - fec/tests/format/bad_bufw.fe:6:26: error: invalid enum variant constructor - 6 | let w = io.buf_writer(buf); - | ^ - ``` - -- 일치 여부: 애매 — 존재하지 않는 `io.buf_writer`를 거부한다는 점은 맞지만, fixture가 검사하려는 구체적인 buffer-writer 규칙을 진단한 것인지 코드만으로 판정할 수 없다. - -### `fec/tests/format/bad_cls.fe` - -- 검사 대상: 닫히지 않은 placeholder가 아니라 unmatched `}` 형식 오류. -- 근거: 4행의 format 문자열이 단독 `}`를 포함한다. -- 실제 진단: - - ```text - fec/tests/format/bad_cls.fe:4:6: error: unmatched '}' in format - 4 | @print("}", 1); - | ^ - fec/tests/format/bad_cls.fe:4:6: error: format argument count mismatch - 4 | @print("}", 1); - | ^ - ``` - -- 일치 여부: 예 — 첫 진단이 unmatched `}`를 직접 짚는다. 두 번째는 파생된 개수 진단이다. - -### `fec/tests/format/bad_many.fe` - -- 검사 대상: placeholder보다 많은 format 인자. -- 근거: 문자열에는 `{}` 하나뿐인데 4행에서 `1, 2` 두 인자를 전달한다. -- 실제 진단: - - ```text - fec/tests/format/bad_many.fe:4:6: error: format argument count mismatch - 4 | @print("{}", 1, 2); - | ^ - ``` - -- 일치 여부: 예 — format 인자 개수 불일치를 직접 진단한다. - -### `fec/tests/format/bad_open.fe` - -- 검사 대상: 닫히지 않은 format placeholder. -- 근거: 4행의 문자열에 여는 `{`만 있고 닫는 `}`가 없다. -- 실제 진단: - - ```text - fec/tests/format/bad_open.fe:4:6: error: unterminated format placeholder - 4 | @print("{", 1); - | ^ - fec/tests/format/bad_open.fe:4:6: error: format argument count mismatch - 4 | @print("{", 1); - | ^ - ``` - -- 일치 여부: 예 — 첫 진단이 종료되지 않은 placeholder를 직접 짚는다. 두 번째는 파생된 개수 진단이다. - -### `fec/tests/format/bad_run.fe` - -- 검사 대상: 런타임 문자열을 format 문자열로 사용. -- 근거: 4행에서 `fmt`를 `var str`로 선언하고 5행에서 `@print(fmt, 1)`에 전달한다. -- 실제 진단: - - ```text - fec/tests/format/bad_run.fe:5:6: error: format must be a comptime string - 5 | @print(fmt, 1); - | ^ - ``` - -- 일치 여부: 예 — format 문자열의 comptime 제약을 직접 진단한다. - -### `fec/tests/format/bad_try.fe` - -- 검사 대상: 오류 결과가 아닌 `@print`에 `try` 사용. -- 근거: 명세상 `@print`은 `void`를 반환하는데 4행에서 `try @print(...)`을 쓴다. -- 실제 진단: - - ```text - fec/tests/format/bad_try.fe:4:5: error: try requires an error result - 4 | try @print("nope"); - | ^ - ``` - -- 일치 여부: 예 — `try`의 오류 결과 요구를 직접 진단한다. - -### `fec/tests/format/bad_type.fe` - -- 검사 대상: format writer가 없는 타입을 format 인자로 사용. -- 근거: `Point` 구조체 값을 7행에서 `{}` placeholder의 인자로 전달한다. -- 실제 진단: - - ```text - fec/tests/format/bad_type.fe:7:18: error: no fmt writer for argument type - 7 | @print("{}", p); - | ^ - ``` - -- 일치 여부: 예 — 해당 타입을 포맷할 writer가 없음을 직접 진단한다. - -### `fec/tests/format/bad_verb.fe` - -- 검사 대상: 지원되지 않는 format verb. -- 근거: 4행의 `{q}`에서 `q`는 명세에 없는 verb다. -- 실제 진단: - - ```text - fec/tests/format/bad_verb.fe:4:6: error: unsupported format verb - 4 | @print("{q}", 1); - | ^ - ``` - -- 일치 여부: 예 — 지원되지 않는 verb를 직접 진단한다. - -### `fec/tests/format/bad_writ.fe` - -- 검사 대상: `@fprint` 첫 인자의 `io.Writer` 타입 위반. -- 근거: 5행에서 writer 대신 `i32` 변수 `x`를 첫 인자로 전달한다. -- 실제 진단: - - ```text - fec/tests/format/bad_writ.fe:5:13: error: @fprint requires io.Writer - 5 | @fprint(x, "bad"); - | ^ - ``` - -- 일치 여부: 예 — `@fprint`의 writer 요구를 직접 진단한다. - -## own - -### `fec/tests/own/bad_clos.fe` - -- 검사 대상: 소유 값을 close 후 다시 사용. -- 근거: 11행의 `try file.close()`가 `file`을 이동시키고 12행에서 다시 `file.close()`를 호출한다. -- 실제 진단: - - ```text - fec/tests/own/bad_clos.fe:12:5: error: use of moved value - 12 | file.close(); - | ^ - fec/tests/own/bad_clos.fe:11:9: note: value was moved here - 11 | try file.close(); - | ^ - fec/tests/own/bad_clos.fe:12:5: error: use of moved value - 12 | file.close(); - | ^ - fec/tests/own/bad_clos.fe:11:9: note: value was moved here - 11 | try file.close(); - | ^ - ``` - -- 일치 여부: 예 — 이동된 값을 재사용한 위치와 이동 위치를 직접 진단한다. 동일 진단이 중복 출력된다. - -### `fec/tests/own/bad_cond.fe` - -- 검사 대상: 조건부 이동 후 값의 무조건 사용. -- 근거: 6행의 조건 분기 안에서 `take(p)`가 `p`를 이동할 수 있고 7행에서 무조건 `p`를 쓴다. -- 실제 진단: - - ```text - fec/tests/own/bad_cond.fe:7:5: error: use of possibly moved value - 7 | p.^ = 3; - | ^ - fec/tests/own/bad_cond.fe:7:5: error: use of possibly moved value - 7 | p.^ = 3; - | ^ - ``` - -- 일치 여부: 예 — 조건부 이동 가능성을 직접 진단한다. 동일 진단이 중복 출력된다. - -### `fec/tests/own/bad_dbl.fe` - -- 검사 대상: 소유 포인터의 이중 destroy. -- 근거: 4행에서 `p`를 destroy한 뒤 5행에서 같은 `p`를 다시 destroy한다. -- 실제 진단: - - ```text - fec/tests/own/bad_dbl.fe:5:17: error: use of moved value - 5 | mem.destroy(p); - | ^ - fec/tests/own/bad_dbl.fe:4:17: note: value was moved here - 4 | mem.destroy(p); - | ^ - fec/tests/own/bad_dbl.fe:5:17: error: use of moved value - 5 | mem.destroy(p); - | ^ - fec/tests/own/bad_dbl.fe:4:17: note: value was moved here - 4 | mem.destroy(p); - | ^ - ``` - -- 일치 여부: 예 — 두 번째 destroy의 이동 후 사용을 직접 진단한다. 동일 진단이 중복 출력된다. - -### `fec/tests/own/bad_dest.fe` - -- 검사 대상: owned pointer가 아닌 값을 `mem.destroy`에 전달. -- 근거: 4행에서 일반 `i32` 값 `x`를 `mem.destroy(x)`에 전달한다. -- 실제 진단: - - ```text - fec/tests/own/bad_dest.fe:4:16: error: mem.destroy requires exactly one owned pointer - 4 | mem.destroy(x); - | ^ - ``` - -- 일치 여부: 예 — `mem.destroy`의 owned pointer 요구를 직접 진단한다. - -### `fec/tests/own/bad_drop.fe` - -- 검사 대상: 사용자 `drop` 메서드의 직접 호출. -- 근거: `Box`에 `drop`을 정의했지만 10행에서 `b.drop()`으로 직접 호출한다. -- 실제 진단: - - ```text - fec/tests/own/bad_drop.fe:10:11: error: drop may only be invoked by scope cleanup - 10 | b.drop(); - | ^ - ``` - -- 일치 여부: 예 — drop은 scope cleanup에서만 호출된다는 규칙을 직접 진단한다. - -### `fec/tests/own/bad_loop.fe` - -- 검사 대상: 반복문 안의 이동으로 인한 possibly-moved 값 사용. -- 근거: 6행의 `while` 본문에서 매 반복 `take(p)`가 `p`를 이동할 수 있다. -- 실제 진단: - - ```text - fec/tests/own/bad_loop.fe:6:24: error: use of possibly moved value - 6 | while again { take(p); } - | ^ - fec/tests/own/bad_loop.fe:6:24: error: use of possibly moved value - 6 | while again { take(p); } - | ^ - ``` - -- 일치 여부: 예 — 반복에 따른 possibly-moved 상태를 직접 진단한다. 동일 진단이 중복 출력된다. - -### `fec/tests/own/bad_move.fe` - -- 검사 대상: 함수 인자의 이중 이동. -- 근거: 6행의 첫 `take(p)`가 `p`를 이동한 뒤 7행에서 다시 `take(p)`를 호출한다. -- 실제 진단: - - ```text - fec/tests/own/bad_move.fe:7:10: error: use of moved value - 7 | take(p); - | ^ - fec/tests/own/bad_move.fe:6:10: note: value was moved here - 6 | take(p); - | ^ - fec/tests/own/bad_move.fe:7:10: error: use of moved value - 7 | take(p); - | ^ - fec/tests/own/bad_move.fe:6:10: note: value was moved here - 6 | take(p); - | ^ - ``` - -- 일치 여부: 예 — 이동된 인자의 재사용과 최초 이동 위치를 직접 진단한다. 동일 진단이 중복 출력된다. - -### `fec/tests/own/bad_proj.fe` - -- 검사 대상: 구조체 projection에서 non-Copy 소유 필드 이동. -- 근거: `Holder.p`는 owned pointer이고 7행에서 `take(h.p)`로 필드 밖으로 직접 이동하려 한다. -- 실제 진단: - - ```text - fec/tests/own/bad_proj.fe:7:11: error: cannot move a non-Copy value out of a projection; use mem.replace - 7 | take(h.p); - | ^ - ``` - -- 일치 여부: 예 — projection에서 non-Copy 값을 이동할 수 없다는 규칙과 대안을 직접 진단한다. - -## 판정 요약 - -- `아니오`: 없음. -- `애매`: `fec/tests/format/bad_bufw.fe` — `io.buf_writer` 자체가 명세에 없으므로 fixture의 구체적 의도를 확정할 수 없음. -- 나머지 36개: 실제 진단이 코드가 검사하려는 규칙과 일치. From 730bcac282bbf579eb064faf216b338da371c03a Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:52:12 +0900 Subject: [PATCH 182/184] =?UTF-8?q?audit:=20SPEC-=ED=8C=8C=EC=84=9C=20?= =?UTF-8?q?=EA=B4=B4=EB=A6=AC=20=EC=9D=BC=EA=B3=B1=EC=9D=84=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구현 셋, SPEC 다섯. 어느 쪽이 틀렸는지는 항목마다 따로 판정했다. 구현: - ~ 를 넣었다. ^ 는 xor 과 .^ 가 가져가서 비트 NOT 이 자기 철자를 못 갖고 있었다. 렉서·파서·검사·lowering(전부 1 과의 xor). - 전역 static/var 는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의 생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. const 는 그대로 추론한다. - error code 는 정수 리터럴 하나다. 식이면 중복도 예약된 0 도 검사할 수 없다. SPEC: - 최상위 comptime if 를 뺐다. comptime 조건은 타입 술어뿐인데(§7.5) 유닛 바깥에는 바인딩된 타입 파라미터가 없어 물어볼 것이 없다. §11 v0.2. - 타입 이름은 [binding.]Name 이다. import 가 마지막 segment 를 바인딩하므로 점 둘 이상은 만들어질 수 없는데 문법이 unit_path 를 쓰고 있었다. - catch 의 EBNF 가 §4.6 보다 넓었다. 값을 주는 짧은 형태와 에러를 받는 블록 형태 둘로 나눠 적었다. - | 와 ^ 를 한 단계로 둔 것을 쪼갰다. 합치면 a | b ^ c 가 좌결합으로 (a|b)^c 가 되어 C 에서 온 사람을 속인다. 구현이 C 순서로 옳았다. - 마지막 필드 쉼표 생략을 명세에 적었다. enum 은 이미 허용하고 있었다. 그리고 tests/run.py 가 마커를 진단 스트림에만 맞춘다. --dump-ast 모드에서는 AST 덤프가 stdout 으로 먼저 나와서 parse/ fixture 는 마커를 쓸 수 없었다. 249/249, 39/39. --- AGENTS.md | 14 ++++++++ SPEC.md | 50 ++++++++++++++++++---------- audits/2026-08-17-spec-fec-parser.md | 19 +++++++++++ fec/src/checkexp.c | 5 +++ fec/src/lexer.c | 2 ++ fec/src/lexer.h | 2 +- fec/src/lowerexp.c | 7 ++++ fec/src/parser.c | 6 ++-- fec/tests/exec/bitnot.fe | 37 ++++++++++++++++++++ fec/tests/parse/badecode.fe | 5 +++ fec/tests/parse/badgtype.fe | 6 ++++ fec/tests/parse/okglobal.fe | 8 +++++ tests/run.py | 8 +++-- 13 files changed, 145 insertions(+), 24 deletions(-) create mode 100644 fec/tests/exec/bitnot.fe create mode 100644 fec/tests/parse/badecode.fe create mode 100644 fec/tests/parse/badgtype.fe create mode 100644 fec/tests/parse/okglobal.fe diff --git a/AGENTS.md b/AGENTS.md index f881759..00e49bc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ DOS/Windows용 시스템 프로그래밍 언어 Ferro와 그 컴파일러 `fec`. | `IR.md` | 중간 표현. 프론트엔드와 기계 사이 | | `TODO.md` | 남은 작업과 정해진 것. 언어 규칙은 `SPEC.md` 를 가리키기만 한다 | | `fec/tests/*/README.md` | 각 fixture 디렉터리가 무엇을 검사하는지 | +| `audits/<날짜>-<주제>.md` | 그때 조사해보니 어땠는지. 불변 기록 | ## 파이프라인 @@ -59,6 +60,19 @@ uv run python tests/build.py <프로그램.fe> # 하나만 빌드해서 돌려 줄 범위로 자르고 -- 주제별로 묶는 것보다 정확하다, 한 줄도 잃거나 겹치지 않으니 -- 공유하는 것은 비공개 헤더(`checkpri.h`, `lowerpri.h`)에 모은다. +## 조사 기록 + +한 번 조사하고 끝나는 것 -- 명세와 구현의 대조, 진단 증거 수집, 외부 감사 -- +은 `audits/<날짜>-<주제>.md`에 남긴다. 날짜와 기준 커밋을 적는다. 조사에서 +나온 **결론**은 `SPEC.md`나 `TODO.md`로 옮기고, audit 자체는 그때 무엇을 +봤는지의 기록으로 둔다. + +본문은 고치지 않는다. 다만 해결되면 맨 위에 **해결 줄 하나**를 붙인다 -- +어느 커밋에서 어떻게 정리됐는지. 그것 없이는 읽는 사람이 아직 살아있는 +문제인지 알 수 없다. + +계획 문서는 두지 않는다 -- 끝난 계획은 git log 다. + ## 작업 흐름 - 명세 판단이 바뀌면 `SPEC.md`를 즉시 갱신한다. 구현이 명세와 다르면 둘 중 하나가 diff --git a/SPEC.md b/SPEC.md index 13b0147..8716434 100644 --- a/SPEC.md +++ b/SPEC.md @@ -333,20 +333,21 @@ unit_path := ident ('.' ident)* import := 'import' unit_path ['as' ident] ';' decl := ['pub'] (fn_decl | struct_decl | enum_decl | error_decl - | const_decl | global_decl) | comptime_decl -comptime_decl := 'comptime' 'if' expr '{' decl* '}' ['else' ('{' decl* '}' | comptime_decl)] + | const_decl | global_decl) fn_decl := ['extern' string] 'fn' ident '(' [param (',' param)*] ')' ['->' type] (block | ';') param := ['comptime'] ident ':' type generic_params := '(' ident (',' ident)* ')' struct_decl := ['packed'] 'struct' ident [generic_params] '{' member* '}' + // field의 마지막 쉼표는 '}' 바로 앞에서 생략할 수 있다 member := ['pub'] (field | fn_decl) field := ident ':' type ',' enum_decl := 'enum' ident [generic_params] '{' variant (',' variant)* [','] '}' variant := ident | ident '(' type ')' | ident '{' vfield* '}' vfield := ident ':' type ',' -error_decl := 'error' ident '{' ident '=' int (',' ident '=' int)* [','] '}' +error_decl := 'error' ident '{' ident '=' int_literal + (',' ident '=' int_literal)* [','] '}' const_decl := 'const' ident [':' type] '=' expr ';' global_decl := 'static' ident ':' type '=' expr ';' | 'var' ident ':' type '=' expr ';' @@ -378,21 +379,30 @@ pattern := ident // 배리언트, 페이로드 없음 | 'Some' '(' ident ')' | 'None' | int_literal | char_literal | 'true' | 'false' | '_' -qualified_name := ident ('.' ident)* -type := qualified_name - | '?' type | '!' type | qualified_name '!' type +type_name := [ident '.'] ident // [binding '.'] Name +type := type_name + | '?' type | '!' type | type_name '!' type | '^' type | '&' ['mut'] type | '*' type | '[' expr ']' type | '[' ']' ['mut'] type | 'fn' '(' [type (',' type)*] ')' ['->' type] - | qualified_name '(' type (',' type)* ')' // 제네릭 인스턴스 + | type_name '(' type (',' type)* ')' // 제네릭 인스턴스 -catch_expr := expr 'catch' ['|' ident '|'] (expr | block) +catch_expr := expr 'catch' expr | expr 'catch' '|' ident '|' block orelse_expr := expr 'orelse' expr ``` `member`의 `pub`은 필드와 메서드 모두에 개별로 붙는다(§8). 필드와 메서드는 순서를 -섞어 쓸 수 있다. `catch`의 블록 형태는 값을 만들지 않으며 §4.6의 규칙을 따른다. -`unit_path` segment의 lexical 제한과 source path 대응은 §8.1이 규정한다. +섞어 쓸 수 있다. `catch`의 두 형태는 서로 다른 일을 한다: 값을 주는 짧은 형태와, +에러를 받아 빠져나가는 블록 형태다. 블록은 값을 만들지 않으므로(§11) 바인딩이 +있는 쪽만 블록을 받는다(§4.6). `unit_path` segment의 lexical 제한과 source path +대응은 §8.1이 규정한다. + +타입 이름은 `[binding '.'] Name`이다. `import`는 unit path의 **마지막 segment**를 +바인딩하므로(§8.2) 점이 둘 이상인 타입 이름은 만들어질 수 없다. `unit_path` +자체는 `import`와 `unit` 선언에서만 쓴다. + +전역 `static`/`var`는 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 초기값의 +생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. `const`는 추론한다. ### 6.2 표현식 우선순위 (낮음 → 높음) @@ -405,20 +415,23 @@ orelse_expr := expr 'orelse' expr 2 or 3 and 4 == != < <= > >= -5 | ^ -6 & -7 << >> -8 + - +% -% -9 * / % *% -10 단항: - not ~ & &mut try -11 후위: .field .? .^ [i] [a..b] (args) as T -12 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) +5 | +6 ^ +7 & +8 << >> +9 + - +% -% +10 * / % *% +11 단항: - not ~ & &mut try +12 후위: .field .? .^ [i] [a..b] (args) as T +13 기본: literal, ident, '(' expr ')', struct_literal, @builtin(...) ``` - `and`, `or`는 단축 평가한다. 좌변이 답을 정하면 우변을 평가하지 않는다. - `orelse`와 `catch`도 lazy다. 좌변이 각각 `Some`/success이면 우변 또는 handler를 평가하지 않는다(§4.5·§4.6). - `as`는 후위 우선순위(단항보다 강함)지만 단항 연산자 바로 뒤에 `as`가 나타나면 모호한 비용을 숨기지 않도록 괄호를 강제한다. `(-x) as u32`와 `-(x as u32)`는 허용하고 `-x as u32`는 컴파일 에러다. +- `|`, `^`, `&`는 서로 다른 단계다. C와 같은 순서이며, 한 단계로 합치면 + `a | b ^ c`가 좌결합으로 `(a | b) ^ c`가 되어 C에서 온 사람을 속인다. - `..`는 일반 표현식 연산자가 아니며 `for` 헤더에서만 쓸 수 있다. - 비교 연산 체이닝 금지(`a < b < c`는 에러). - `.field`, `[i]`, `[a..b]`, 메서드 호출은 `&`, `&mut`, `^`를 필요한 만큼 자동 projection한다. 값 자체의 역참조는 `.^`가 필요하며 raw `*T`와 optional `?T`는 자동 역참조하지 않는다. @@ -791,6 +804,7 @@ binding은 마지막 segment라 `io.write`, `mem.replace` 형태로 사용한다 | GC | 영구 | 결정적 비용 원칙 위반 | 소유권 + RAII + 아레나 | | 암묵 형변환 | 영구 | 버그 원인 1위 | `as` | | 상속 | 영구 | 숨은 vtable, 취약한 기반 클래스 | 합성 | +| 최상위 `comptime if` | v0.2 | 타깃이 하나이고 comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다 | 함수 안의 `comptime if`, 또는 유닛을 나눈다 | ### 11.1 인터페이스 설계 스케치 (v0.2 예정) diff --git a/audits/2026-08-17-spec-fec-parser.md b/audits/2026-08-17-spec-fec-parser.md index 0a406b4..933200e 100644 --- a/audits/2026-08-17-spec-fec-parser.md +++ b/audits/2026-08-17-spec-fec-parser.md @@ -3,6 +3,8 @@ - 날짜: 2026-08-17 - 기준 커밋: `52aaff62e490e37a0995aaaf7cbda47cf98e54a7` - 범위: `SPEC.md` §6과 `fec/src/lexer.c`, `fec/src/parser.c` +- **해결: 일곱 전부. 구현 셋(PARSE-04a·05·06), SPEC 다섯(01·02·03·04b·07).** + 판정과 근거는 아래 표에 덧붙였다. 본문은 조사 시점 그대로다. ## 현재 문제 @@ -16,6 +18,23 @@ | PARSE-06 | error code는 정수 literal | 일반 expression을 파싱하며 literal이 아니면 code 검증을 건너뜀 | `error E { Bad = 1 + 2, }`가 검사 통과 | | PARSE-07 | struct field와 enum vfield의 쉼표 필수 | 닫는 `}` 바로 앞에서는 쉼표 생략 허용 | `struct S { x: i32 }`가 검사 통과 | +## 해결 + +| ID | 어느 쪽이 틀렸나 | 무엇을 했나 | +|---|---|---| +| PARSE-01 | SPEC | comptime 조건은 타입 술어뿐이라(§7.5) 유닛 바깥에는 물어볼 것이 없다. `comptime_decl` 을 `decl` 에서 빼고 §11 v0.2 로 | +| PARSE-02 | SPEC | `import` 는 unit path 의 마지막 segment 를 바인딩하므로 점 둘 이상인 타입 이름은 만들어질 수 없다. 문법을 `type_name := [ident '.'] ident` 로 | +| PARSE-03 | SPEC | §4.6 과 §11(블록 표현식 배제)이 실제 규칙이고 EBNF 가 넓었다. 두 형태로 나눠 적었다 | +| PARSE-04a | 구현 | `~` 를 넣었다. 렉서·파서·검사·lowering(`xor` with all ones) | +| PARSE-04b | SPEC | `\|` 와 `^` 를 한 단계로 두면 `a \| b ^ c` 가 `(a\|b)^c` 가 되어 C 에서 온 사람을 속인다. 구현(C 순서)이 옳아서 표를 쪼갰다 | +| PARSE-05 | 구현 | 전역 `static`/`var` 는 타입 필수. `const` 는 그대로 추론 | +| PARSE-06 | 구현 | error code 는 정수 리터럴 하나만 받는다 | +| PARSE-07 | SPEC | 마지막 쉼표 생략은 흔하고 `enum` 은 이미 허용하고 있었다. 명세에 적었다 | + +fixture: `parse/badgtype.fe`, `parse/badecode.fe`, `parse/okglobal.fe`, +`exec/bitnot.fe`. 그리고 `tests/run.py` 가 마커를 진단 스트림에만 맞춘다 -- +`--dump-ast` 모드에서 AST 덤프가 먼저 나와 마커가 못 쓰이고 있었다. + ## 검증 - `uv run python tests/run.py`: `240/240` 통과 diff --git a/fec/src/checkexp.c b/fec/src/checkexp.c index 1a32aa5..bf154a6 100644 --- a/fec/src/checkexp.c +++ b/fec/src/checkexp.c @@ -371,6 +371,11 @@ FeType *check_expr_core(FeCheckerState *s, FeNode *n) } else if (strcmp(op, "-") == 0) { if (known(a) && !fe_type_is_integer(a)) err(c, n->loc, "unary '-' requires integer"); + } else if (strcmp(op, "~") == 0) { + /* Flipping every bit only means something where the bits are the + value (SPEC 6.2). */ + if (known(a) && !fe_type_is_integer(a)) + err(c, n->loc, "unary '~' requires integer"); } else if (strcmp(op, "try") == 0) { /* SPEC 6.4: try is only allowed inside a function returning an error union. Checked on the expression rather than on the statement so diff --git a/fec/src/lexer.c b/fec/src/lexer.c index 2b25bfe..20866d7 100644 --- a/fec/src/lexer.c +++ b/fec/src/lexer.c @@ -145,6 +145,7 @@ FeToken fe_lexer_next(FeLexer *l) case '&': if(cur(l)=='&'){advance(l);fe_diag_error(l->diags,here(l,line,col),"&& is not a Ferro logical operator; use 'and'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_AND_EQ,start,line,col);} return tok(l,FE_TOK_AND,start,line,col); case '|': if(cur(l)=='|'){advance(l);fe_diag_error(l->diags,here(l,line,col),"|| is not a Ferro logical operator; use 'or'");return tok(l,FE_TOK_UNKNOWN,start,line,col);} if(cur(l)=='='){advance(l);return tok(l,FE_TOK_OR_EQ,start,line,col);} return tok(l,FE_TOK_OR,start,line,col); case '^': if(cur(l)=='='){advance(l);return tok(l,FE_TOK_XOR_EQ,start,line,col);} return tok(l,FE_TOK_XOR,start,line,col); + case '~': return tok(l,FE_TOK_TILDE,start,line,col); default: fe_diag_error(l->diags,here(l,line,col),"unknown character"); return tok(l,FE_TOK_UNKNOWN,start,line,col); } } @@ -162,6 +163,7 @@ const char *fe_token_name(FeTokKind k) case FE_TOK_TRUE:return "true"; case FE_TOK_FALSE:return "false"; case FE_TOK_NULL:return "null"; case FE_TOK_UNDEFINED:return "undefined"; case FE_TOK_AND_KW:return "and"; case FE_TOK_OR_KW:return "or"; case FE_TOK_NOT:return "not"; case FE_TOK_BANG:return "!"; + case FE_TOK_TILDE:return "~"; case FE_TOK_LBRACE:return "{"; case FE_TOK_RBRACE:return "}"; case FE_TOK_LPAREN:return "("; case FE_TOK_RPAREN:return ")"; case FE_TOK_SEMI:return ";"; case FE_TOK_COLON:return ":"; case FE_TOK_COMMA:return ","; case FE_TOK_EQ:return "="; case FE_TOK_ARROW:return "->"; case FE_TOK_FATARROW:return "=>"; diff --git a/fec/src/lexer.h b/fec/src/lexer.h index 5c3bb47..c60c2ba 100644 --- a/fec/src/lexer.h +++ b/fec/src/lexer.h @@ -20,7 +20,7 @@ typedef enum FeTokKind { FE_TOK_PLUS_EQ, FE_TOK_MINUS_EQ, FE_TOK_STAR_EQ, FE_TOK_SLASH_EQ, FE_TOK_PERCENT_EQ, FE_TOK_PLUS_WRAP, FE_TOK_MINUS_WRAP, FE_TOK_STAR_WRAP, FE_TOK_EQ, FE_TOK_EQEQ, FE_TOK_NE, FE_TOK_LT, FE_TOK_LE, FE_TOK_GT, FE_TOK_GE, - FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR, + FE_TOK_AND, FE_TOK_OR, FE_TOK_AND_KW, FE_TOK_OR_KW, FE_TOK_XOR, FE_TOK_TILDE, FE_TOK_NOT, FE_TOK_BANG, FE_TOK_SHL, FE_TOK_SHR, FE_TOK_AND_EQ, FE_TOK_OR_EQ, FE_TOK_XOR_EQ, FE_TOK_SHL_EQ, FE_TOK_SHR_EQ, FE_TOK_ANDAND, FE_TOK_OROR, FE_TOK_ARROW, FE_TOK_FATARROW, FE_TOK_AT, FE_TOK_QUESTION, FE_TOK_UNKNOWN diff --git a/fec/src/lowerexp.c b/fec/src/lowerexp.c index e457fa7..ecaa36a 100644 --- a/fec/src/lowerexp.c +++ b/fec/src/lowerexp.c @@ -117,6 +117,13 @@ Slot lower_expr_core(Lower *L, FeNode *n) return slot_value(fe_ir_binary(L->m, L->b, FE_IR_SUB, it, zero, v, 0), it); } + if (n->text && !strcmp(n->text, "~")) { + /* Every bit flipped is every bit exchanged with a one. */ + unsigned ones = fe_ir_const(L->m, L->b, it, -1L); + unsigned v = as_value(L, lower_expr(L, n->a), n->a); + return slot_value(fe_ir_binary(L->m, L->b, FE_IR_XOR, it, v, ones, + 0), it); + } if (n->text && !strcmp(n->text, "not")) { unsigned zero = fe_ir_const(L->m, L->b, FE_IR_I8, 0); unsigned v = as_value(L, lower_expr(L, n->a), n->a); diff --git a/fec/src/parser.c b/fec/src/parser.c index 888f5f6..836babf 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -167,7 +167,7 @@ static FeNode *postfix(FeParser *p) static FeNode *expr(FeParser *p, int minprec) { FeToken t=p->current; FeNode *left,*n; int prec; - if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; } + if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_TILDE)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; } else left=postfix(p); for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break;next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; } return left; @@ -228,9 +228,9 @@ static FeNode *decl(FeParser *p) if(eat(p,FE_TOK_PACKED)) t=p->previous; if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } - if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");v->a=expr(p,0);want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } + if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");if(is(p,FE_TOK_INT)){v->a=toknode(p,FE_N_LITERAL,p->current);next(p);}else error(p,"an error code must be an integer literal");if(!is(p,FE_TOK_COMMA)&&!is(p,FE_TOK_RBRACE)){error(p,"an error code must be an integer literal");recover(p);break;}want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } - if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } + if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);else if(kk!=FE_TOK_CONST)error(p,"a global declaration requires an explicit type");want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } error(p,"expected declaration"); before=p->current.kind; recover(p); if (p->current.kind==before && p->current.kind!=FE_TOK_EOF) next(p); return 0; diff --git a/fec/tests/exec/bitnot.fe b/fec/tests/exec/bitnot.fe new file mode 100644 index 0000000..426b970 --- /dev/null +++ b/fec/tests/exec/bitnot.fe @@ -0,0 +1,37 @@ +// EXIT:0 +// OUTPUT:i32 -1 -6 4 +// OUTPUT:u8 255 250 +// OUTPUT:mask 240 15 +// OUTPUT:prec 7 3 +unit bitnot; + +import std.io; + +// `~` flips every bit (SPEC §6.2, 단항). `^` is taken by xor and by `.^`, so +// bitwise NOT needs its own spelling. +// +// `|`, `^` and `&` are three different levels, in C's order. Merging `|` and +// `^` would make `a | b ^ c` bind as `(a | b) ^ c`, which is not what anyone +// coming from C reads it as. + +fn main() -> i32 { + let a: i32 = 0; + let b: i32 = 5; + let c: i32 = -5; + @print("i32 {} {} {}\n", ~a, ~b, ~c); + + let u: u8 = 0; + let v: u8 = 5; + @print("u8 {} {}\n", (~u) as i32, (~v) as i32); + + // Clearing bits is what the operator is for. + let bits: u8 = 255; + let low: u8 = 15; + @print("mask {} {}\n", (bits & ~low) as i32, (bits & low) as i32); + + // 1 | 2 ^ 4 is 1 | (2 ^ 4) = 1 | 6 = 7, not (1 | 2) ^ 4 = 3 ^ 4 = 7. + // Those agree, so pick operands that do not: 3 | 1 ^ 2 is 3 | 3 = 3, + // while (3 | 1) ^ 2 would be 3 ^ 2 = 1. The 3 is the proof. + @print("prec {} {}\n", 1 | 2 ^ 4, 3 | 1 ^ 2); + return 0; +} diff --git a/fec/tests/parse/badecode.fe b/fec/tests/parse/badecode.fe new file mode 100644 index 0000000..eab8829 --- /dev/null +++ b/fec/tests/parse/badecode.fe @@ -0,0 +1,5 @@ +// ERROR:5:integer literal +unit badecode; + +// SPEC §6.1: error code 는 정수 리터럴이다. 식이면 중복도 예약된 0 도 검사할 수 없다. +error E { Bad = 1 + 2, } diff --git a/fec/tests/parse/badgtype.fe b/fec/tests/parse/badgtype.fe new file mode 100644 index 0000000..ed9c9a1 --- /dev/null +++ b/fec/tests/parse/badgtype.fe @@ -0,0 +1,6 @@ +// ERROR:6:requires an explicit type +unit badgtype; + +// SPEC §6.1: 전역은 타입을 적는다. 다른 유닛이 읽는 링커 심볼이라 +// 초기값의 생김새에 타입을 맡기면 그쪽이 보는 것이 달라진다. +static A = 1; diff --git a/fec/tests/parse/okglobal.fe b/fec/tests/parse/okglobal.fe new file mode 100644 index 0000000..02443a6 --- /dev/null +++ b/fec/tests/parse/okglobal.fe @@ -0,0 +1,8 @@ +unit okglobal; + +// `const` 는 여전히 추론한다 -- SPEC §6.1 이 타입을 요구하는 것은 전역 둘뿐이다. +const A = 1; +static B: i32 = 2; +var C: i32 = 3; + +error E { Bad = 1, Worse = 0x10, } diff --git a/tests/run.py b/tests/run.py index 34b3661..8cb5d84 100644 --- a/tests/run.py +++ b/tests/run.py @@ -82,6 +82,10 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]: f"--std={ROOT / 'fec'}"], capture_output=True, text=True, timeout=30) output = (done.stdout + done.stderr).strip() + # Diagnostics go to the diag stream. `--dump-ast` also writes the tree to + # stdout, so a marker matched against the two together would read the tree + # and never reach the error. + diags = done.stderr.strip() or output rejected = done.returncode != 0 if want.rejected != rejected: @@ -96,13 +100,13 @@ def run_case(fec: Path, path: Path) -> tuple[bool, str]: return True, "" # The marker pins where and roughly what, so a rule can be moved or reworded # only deliberately. - first = output.split("\n", 1)[0] if output else "" + first = diags.split("\n", 1)[0] if diags else "" at = re.search(r":(\d+):\d+: error:", first) if not at: return False, f"no diagnostic to match marker\n got: {first or '(silent)'}" if int(at.group(1)) != want.line: return False, f"marker says line {want.line}, diagnostic is line {at.group(1)}\n {first}" - if want.text and want.text.lower() not in output.lower(): + if want.text and want.text.lower() not in diags.lower(): return False, f"marker wants {want.text!r}\n {first}" return True, "" From c7a1b98654a3b78f4f435ec882bdecd089883df7 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 16:52:30 +0900 Subject: [PATCH 183/184] =?UTF-8?q?audit:=20=ED=94=84=EB=9F=B0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EB=B9=88=ED=8B=88=20=EC=A1=B0=EC=82=AC?= =?UTF-8?q?=EB=A5=BC=20=EA=B8=B0=EB=A1=9D=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parser, checker, 전역 lowering 의 경계에서 11 건. 기준 커밋 6dc298d. --- audits/2026-08-17-frontend-gaps.md | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 audits/2026-08-17-frontend-gaps.md diff --git a/audits/2026-08-17-frontend-gaps.md b/audits/2026-08-17-frontend-gaps.md new file mode 100644 index 0000000..7be90b3 --- /dev/null +++ b/audits/2026-08-17-frontend-gaps.md @@ -0,0 +1,46 @@ +# Frontend gap audit + +- 날짜: 2026-08-17 +- 기준 커밋: `6dc298d828872409fdf6b7d2e85830f18a118d9f` +- 범위: parser, checker, 전역 lowering의 경계 + +## 재현된 문제 + +아래 최소 입력은 발견 시점의 `fec --check`를 모두 통과했다. + +| ID | 문제 | 필요한 fixture | +|---|---|---| +| FRONT-01 | runtime 호출을 `const` 초기값으로 허용하고 lowering에서 초기값을 방출하지 않음 | `types/badcini.fe` | +| FRONT-02 | runtime 호출을 `static` 초기값으로 허용하고 저장소를 0으로 초기화 | `types/badsini.fe` | +| FRONT-03 | bool 비교 체이닝 허용: `true == false == true` | `types/badchain.fe` | +| FRONT-04 | 괄호 없는 단항식 뒤 cast 허용: `-x as u32` | `types/badunas.fe` | +| FRONT-05 | `unsafe` 밖에서 `asm` 허용 | `types/badasm.fe` | +| FRONT-06 | ABI 문자열 없는 `extern fn f();` 허용 | `types/badexns.fe` | +| FRONT-07 | `extern "c"` 이외 ABI 문자열 허용 | `types/badexab.fe` | +| FRONT-08 | extern 함수 본문 허용 | `types/badexbd.fe` | +| FRONT-09 | `extern`이 아닌 본문 없는 `fn f();`를 외부 심볼로 처리 | `types/badfnsm.fe` | +| FRONT-10 | 빈 enum 선언 허용 | `parse/bademen.fe` | +| FRONT-11 | 빈 error 선언 허용 | `parse/bademer.fe` | + +중복 struct field와 중복 enum variant 선언도 통과했지만, 중복 선언 규칙을 SPEC에서 먼저 +확정해야 하므로 위 목록에는 넣지 않았다. + +## 이미 알려진 실행 문제 + +`0b`와 `0o` 리터럴은 lexer가 받지만 값 계산이 진법을 반영하지 않는다. 실행 결과를 +고정하는 `exec/radix.fe`가 필요하다. + +## 테스트 기반의 빈틈 + +`parse/` fixture는 `--dump-ast`의 성공 여부만 검사하고 트리 내용은 비교하지 않는다. +따라서 연산자 우선순위나 postfix 결합 방향은 parse fixture만으로 고정되지 않는다. +이런 항목은 실행 결과로 구별하거나 선택적인 AST 기대값 검사를 추가해야 한다. + +## 검증 + +- `uv run python tests/run.py`: `245/245` 통과 +- 각 문제를 독립적인 최소 입력으로 만들어 `--check` 결과를 확인함 +- 조사용 임시 입력은 제거함 + +이 문서는 발견 시점의 상태를 기록한다. 작성 중인 미커밋 수정으로 일부 항목의 상태가 +바뀔 수 있으므로 해결 여부는 fixture와 두 테스트 suite로 확인한다. From b4f947b64388011bb80eac301d356bd48ccbbef1 Mon Sep 17 00:00:00 2001 From: Sebastian Jeong Date: Mon, 17 Aug 2026 17:00:29 +0900 Subject: [PATCH 184/184] =?UTF-8?q?audit:=20=ED=94=84=EB=9F=B0=ED=8A=B8?= =?UTF-8?q?=EC=97=94=EB=93=9C=20=EB=B9=88=ED=8B=88=20=EC=97=B4=ED=95=98?= =?UTF-8?q?=EB=82=98=EC=99=80=20=EC=A7=84=EB=B2=95=20=EB=A6=AC=ED=84=B0?= =?UTF-8?q?=EB=9F=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 전부 구현 쪽이었다. SPEC 은 §6.2(체이닝·단항 뒤 as), §5 R9(asm), §6.1·§7.3 (extern, 빈 enum/error)을 이미 옳게 적고 있었고 구현만 따라가지 않았다. const A: i32 = r(); 전역 초기값에 호출. lowering 이 조용히 버리고 0 static A: i32 = r(); 같은 것을 저장소 0 으로 true == false == true 비교 체이닝 -x as u32 괄호 없이 단항 뒤 as asm { } unsafe 밖에서 extern fn f(); ABI 문자열 없이 extern "stdcall" fn f(); c 아닌 ABI extern "c" fn f() { } extern 에 본문 fn f() -> i32; extern 아닌데 본문 없음 enum E { } error E { } 빈 선언 FRONT-01/02 는 SPEC 에도 규칙이 없었다. §7.1 에 넣었다 -- 전역의 바이트는 이미지에 들어가므로 초기값이 실행될 순간이 없다. 리터럴과 다른 const, 배리언트, error.Name, 그리고 그것들에 대한 연산까지가 허용된다. -x as T 와 비교 체이닝을 잡으려면 괄호가 트리에 남아야 해서 FE_NODE_PAREN 을 두었다. 파싱 뒤에는 -x as T 와 -(x as T) 가 같은 트리다. 그리고 0b1010 이 0, 0o17 이 0 이었다. 10진으로 읽다가 b 에서 멈춰 0 을 내는데 0 도 숫자라 아무도 눈치채지 못한다. 값 계산 두 군데를 고쳤다. 262/262, 40/40. --- SPEC.md | 1 + TODO.md | 1 - audits/2026-08-17-frontend-gaps.md | 31 +++++++++++++++++++ fec/src/ast.h | 3 ++ fec/src/checkcal.c | 6 ++++ fec/src/checkgen.c | 44 +++++++++++++++++++++++++++ fec/src/checkpri.h | 3 ++ fec/src/checkpro.c | 4 +++ fec/src/checkstm.c | 29 ++++++++++-------- fec/src/lower.c | 26 ++++++++-------- fec/src/parser.c | 49 +++++++++++++++++++++++++----- fec/tests/exec/radix.fe | 29 ++++++++++++++++++ fec/tests/parse/bademen.fe | 3 ++ fec/tests/parse/bademer.fe | 3 ++ fec/tests/types/badasm.fe | 3 ++ fec/tests/types/badchain.fe | 3 ++ fec/tests/types/badcini.fe | 4 +++ fec/tests/types/badexab.fe | 3 ++ fec/tests/types/badexbd.fe | 3 ++ fec/tests/types/badexns.fe | 3 ++ fec/tests/types/badfnsm.fe | 3 ++ fec/tests/types/badsini.fe | 4 +++ fec/tests/types/badunas.fe | 3 ++ fec/tests/types/okglobin.fe | 23 ++++++++++++++ 24 files changed, 252 insertions(+), 32 deletions(-) create mode 100644 fec/tests/exec/radix.fe create mode 100644 fec/tests/parse/bademen.fe create mode 100644 fec/tests/parse/bademer.fe create mode 100644 fec/tests/types/badasm.fe create mode 100644 fec/tests/types/badchain.fe create mode 100644 fec/tests/types/badcini.fe create mode 100644 fec/tests/types/badexab.fe create mode 100644 fec/tests/types/badexbd.fe create mode 100644 fec/tests/types/badexns.fe create mode 100644 fec/tests/types/badfnsm.fe create mode 100644 fec/tests/types/badsini.fe create mode 100644 fec/tests/types/badunas.fe create mode 100644 fec/tests/types/okglobin.fe diff --git a/SPEC.md b/SPEC.md index 8716434..36253de 100644 --- a/SPEC.md +++ b/SPEC.md @@ -546,6 +546,7 @@ pub fn main() -> !void { - `&mut x`, mutable slice 생성과 `&mut Self` 메서드 호출은 `var` place에서만 가능하다. `let`이 `^T`를 보유해도 그 대상을 안전 코드에서 변경할 수 없다. by-value `self: Self`는 소비 메서드 안에서 자신의 필드를 무효 상태로 바꿀 수 있는 가변 local owner로 취급한다. - 모든 변수는 사용 전 초기화 필수(정적 검사). 명시적 미초기화는 `= undefined`(unsafe 아님, 단 읽기 전 쓰기 필수는 여전히 검사). - 섀도잉 허용(같은 스코프에서 `let` 재선언). +- **전역 `const`/`static`/`var`의 초기값은 컴파일 시점에 알 수 있어야 한다.** 저장소가 이미지에 들어가므로 초기값이 실행될 순간이 없다. 리터럴, 다른 `const`, 열거형 배리언트, `error.Name`, 그리고 그것들에 대한 연산과 캐스트·집합체 리터럴까지가 허용되며 함수 호출은 허용되지 않는다. 실행 시점에 계산해야 하는 값은 `main`에서 만든다. ### 7.2 제어 흐름 diff --git a/TODO.md b/TODO.md index 0db2f06..8cf6d8c 100644 --- a/TODO.md +++ b/TODO.md @@ -56,7 +56,6 @@ uv run python tests/exec.py 38/38 컴파일된 프로그램이 실제로 | 컨테이너 두 원소의 동시 `&mut` | 인덱스는 갈라지지 않는다. `swap` 같은 것은 stdlib 안에서 해결한다 | | `--strip-error-names` | 받아들이지만 아무것도 하지 않는다 (SPEC §4.6) | | `fmt.fmt_error` | 없다. SPEC §4.6 이 약속만 하고 있다 | -| `0b` / `0o` 리터럴 | 렉서는 받지만 값 계산이 10진과 16진만 안다 | --- diff --git a/audits/2026-08-17-frontend-gaps.md b/audits/2026-08-17-frontend-gaps.md index 7be90b3..5312b13 100644 --- a/audits/2026-08-17-frontend-gaps.md +++ b/audits/2026-08-17-frontend-gaps.md @@ -3,6 +3,8 @@ - 날짜: 2026-08-17 - 기준 커밋: `6dc298d828872409fdf6b7d2e85830f18a118d9f` - 범위: parser, checker, 전역 lowering의 경계 +- **해결: 11 건 전부와 `0b`/`0o` 리터럴까지. 모두 구현 쪽이었다.** + fixture 는 아래 표에 적었다. 본문은 조사 시점 그대로다. ## 재현된 문제 @@ -25,6 +27,35 @@ 중복 struct field와 중복 enum variant 선언도 통과했지만, 중복 선언 규칙을 SPEC에서 먼저 확정해야 하므로 위 목록에는 넣지 않았다. +## 해결 + +SPEC 은 FRONT-03·04(§6.2), 05(§5 R9), 06~09(§6.1·§7.3), 10·11(§6.1)을 이미 +옳게 적고 있었다. 구현만 따라가지 않았다. FRONT-01·02 는 SPEC 에도 규칙이 +없어서 §7.1 에 문장을 넣었다 -- 전역 초기값은 컴파일 시점에 알 수 있어야 한다. + +| ID | fixture | +|---|---| +| FRONT-01 | `types/badcini.fe` | +| FRONT-02 | `types/badsini.fe` | +| FRONT-03 | `types/badchain.fe` | +| FRONT-04 | `types/badunas.fe` | +| FRONT-05 | `types/badasm.fe` | +| FRONT-06 | `types/badexns.fe` | +| FRONT-07 | `types/badexab.fe` | +| FRONT-08 | `types/badexbd.fe` | +| FRONT-09 | `types/badfnsm.fe` | +| FRONT-10 | `parse/bademen.fe` | +| FRONT-11 | `parse/bademer.fe` | +| 허용되는 짝 | `types/okglobin.fe` | +| `0b`/`0o` | `exec/radix.fe` | + +`-x as T` 와 비교 체이닝을 구별하려면 괄호가 트리에 남아야 해서 노드에 +`FE_NODE_PAREN` 을 두었다. 파싱 뒤에는 `-x as T` 와 `-(x as T)` 가 같은 +트리다. + +남은 것: `parse/` fixture 가 트리 내용을 비교하지 않는다는 지적은 그대로 +유효하다. 우선순위는 지금 `exec/bitnot.fe` 처럼 실행 결과로 구별한다. + ## 이미 알려진 실행 문제 `0b`와 `0o` 리터럴은 lexer가 받지만 값 계산이 진법을 반영하지 않는다. 실행 결과를 diff --git a/fec/src/ast.h b/fec/src/ast.h index b53221c..f13f657 100644 --- a/fec/src/ast.h +++ b/fec/src/ast.h @@ -47,6 +47,9 @@ struct FeNode { /* An index expression that had `..` in it, so it makes a slice rather than reaching an element. `x[a]` and `x[a..]` are otherwise the same shape. */ #define FE_NODE_SLICE 0x40U +/* This expression was written inside parentheses. `-x as T` is a mistake + and `-(x as T)` is not, and after parsing they are the same tree. */ +#define FE_NODE_PAREN 0x80U typedef struct FeAst { FeArena arena; diff --git a/fec/src/checkcal.c b/fec/src/checkcal.c index b27b096..2347b2e 100644 --- a/fec/src/checkcal.c +++ b/fec/src/checkcal.c @@ -1035,7 +1035,13 @@ void check_stmt(FeCheckerState *s, FeNode *n) err(s->c,n->loc,"break or continue outside loop"); break; case FE_N_UNSAFE: + ++s->unsafe_depth; check_stmt(s,n->a); + --s->unsafe_depth; + break; + case FE_N_ASM: + if (!s->unsafe_depth) + err(s->c,n->loc,"asm requires an unsafe block"); break; default: check_stmt_core(s,n); diff --git a/fec/src/checkgen.c b/fec/src/checkgen.c index e2432df..37522fe 100644 --- a/fec/src/checkgen.c +++ b/fec/src/checkgen.c @@ -413,6 +413,50 @@ FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok) return unknown(c); } +/* Can this initializer be worked out before the program runs? + + A global's bytes go into the image, so there is no moment at which a call in + its initializer could happen -- the emitter had been quietly dropping the + work and leaving zeros. Anything that is a name for a value already known is + fine; anything that is work is not. */ +int const_foldable(FeCheckerState *s, FeNode *n) +{ + FeNode *x; + if (!n) return 1; + switch (n->kind) { + case FE_N_LITERAL: + return 1; + case FE_N_IDENT: { + /* Another `const` is a name for a value; a `static`/`var` is storage + that does not exist yet. */ + FeSym *sym=find_symbol(s->scope,n->text ? n->text : ""); + return sym && sym->decl && sym->decl->kind==FE_N_CONST; + } + case FE_N_MEMBER: + /* `E.Variant`, `error.Name`, `unit.CONST` -- a name, not work. */ + if (n->a && n->a->kind==FE_N_IDENT) return 1; + return const_foldable(s,n->a); + case FE_N_UNARY: + if (n->text && strcmp(n->text,"try")==0) return 0; + return const_foldable(s,n->a); + case FE_N_BINARY: + if (n->text && (strcmp(n->text,"catch")==0 || + strcmp(n->text,"orelse")==0)) return 0; + return const_foldable(s,n->a) && const_foldable(s,n->b); + case FE_N_TYPE: + return const_foldable(s,n->a); + case FE_N_EXPR: + return const_foldable(s,n->a); + case FE_N_STRUCT_INIT: + case FE_N_ARRAY_INIT: + for (x=n->children;x;x=x->next) + if (!const_foldable(s,x->kind==FE_N_FIELD ? x->a : x)) return 0; + return 1; + default: + return 0; + } +} + /* A `comptime if` condition. Only the forms SPEC 9 allows: type equality and the type predicates. Anything else is not decidable here. */ int comptime_condition(FeCheckerState *s, FeNode *n, int *out) diff --git a/fec/src/checkpri.h b/fec/src/checkpri.h index 9e59b2e..5c112a3 100644 --- a/fec/src/checkpri.h +++ b/fec/src/checkpri.h @@ -52,6 +52,8 @@ typedef struct FeCheckerState { FeType *ret; unsigned loop_depth; unsigned defer_depth; + /* SPEC 5 R9 lists what only `unsafe` allows; `asm` is on it. */ + unsigned unsafe_depth; FeOwnLiveness liveness; FeNode *fn_node; /* While a projection is being checked, which field of which base it @@ -212,6 +214,7 @@ FeType *instantiate_struct(FeCheck *c, FeUnit *home, const char *name, FeType *instantiate_type_node(void *owner, const FeNode *node); FeType *type_from_expr(FeCheckerState *s, FeNode *n, int *ok); int comptime_condition(FeCheckerState *s, FeNode *n, int *out); +int const_foldable(FeCheckerState *s, FeNode *n); void instantiate_body(FeCheck *c, FeUnit *home, FeNode *decl, FeType *owner, FeBindSave *bindings, FeLoc site); FeType *check_generic_call(FeCheckerState *s, FeNode *n, FeSym *sym, diff --git a/fec/src/checkpro.c b/fec/src/checkpro.c index 0d67cd5..e3a5f80 100644 --- a/fec/src/checkpro.c +++ b/fec/src/checkpro.c @@ -131,6 +131,9 @@ void check_unit_bodies(FeCheck *c, FeCheckerState *s) sym=find_current(s->globals,n->text ? n->text : ""); if (n->kind==FE_N_CONST && const_names_type(s,n)) continue; if (n->b) { + if (!const_foldable(s,n->b)) + err(c,n->b->loc, + "a global initializer must be known at compile time"); iv=m7_check_expected(s,n->b,sym ? sym->type : 0); if (sym && sym->type->kind==FE_TYPE_UNKNOWN) { sym->type=iv; @@ -162,6 +165,7 @@ int fe_check_program(FeCheck *c) s.ret=fe_type_intern(&c->types,"void"); s.loop_depth=0; s.defer_depth=0; + s.unsafe_depth=0; s.fn_node=0; fe_own_liveness_init(&s.liveness,&c->arena); for (u=0;ubuild->count;++u) { enter_unit(c,u); declare_unit(c); } diff --git a/fec/src/checkstm.c b/fec/src/checkstm.c index ae79b49..e5f1aa1 100644 --- a/fec/src/checkstm.c +++ b/fec/src/checkstm.c @@ -488,6 +488,10 @@ void check_stmt_core(FeCheckerState *s, FeNode *n) !compatible(s->ret,b,n->a)) err(c, n->loc, "return type mismatch"); break; + case FE_N_ASM: + if (!s->unsafe_depth) + err(c,n->loc,"asm requires an unsafe block"); + break; case FE_N_UNSAFE: check_stmt(s, n->a); break; @@ -508,6 +512,7 @@ void check_fn(FeCheck *c, FeNode *fn, FeScope *globals) s.ret = fn->b ? node_type(c, fn->b) : fe_type_intern(&c->types, "void"); s.loop_depth=0; s.defer_depth=0; + s.unsafe_depth=0; s.fn_node=fn; fe_own_liveness_init(&s.liveness,&c->arena); fe_own_collect_last_uses(&s.liveness,fn); @@ -546,6 +551,7 @@ void check_method(FeCheck *c, FeNode *fn, FeScope *globals, s.ret=fn->b ? method_type(c,fn->b,owner) : fe_type_intern(&c->types,"void"); s.loop_depth=0; s.defer_depth=0; + s.unsafe_depth=0; s.fn_node=fn; fe_own_liveness_init(&s.liveness,&c->arena); fe_own_collect_last_uses(&s.liveness,fn); @@ -572,21 +578,20 @@ int m7_actual_compatible(FeType *want, FeType *got, FeNode *value) static unsigned long literal_magnitude(const char *s) { unsigned long v = 0; + unsigned long base = 10UL; if (!s) return 0; - if (s[0]=='0' && (s[1]=='x' || s[1]=='X')) { - for (s += 2; *s; ++s) { - int d = *s>='0'&&*s<='9' ? *s-'0' : - *s>='a'&&*s<='f' ? *s-'a'+10 : - *s>='A'&&*s<='F' ? *s-'A'+10 : -1; - if (d < 0) { if (*s=='_') continue; break; } - v = v*16UL + (unsigned long)d; - } - return v; - } + if (s[0]=='0' && (s[1]=='x' || s[1]=='X')) { base = 16UL; s += 2; } + else if (s[0]=='0' && (s[1]=='b' || s[1]=='B')) { base = 2UL; s += 2; } + else if (s[0]=='0' && (s[1]=='o' || s[1]=='O')) { base = 8UL; s += 2; } for (; *s; ++s) { + unsigned long d; if (*s=='_') continue; - if (*s<'0' || *s>'9') break; - v = v*10UL + (unsigned long)(*s-'0'); + if (*s>='0' && *s<='9') d = (unsigned long)(*s-'0'); + else if (*s>='a' && *s<='f') d = (unsigned long)(*s-'a'+10); + else if (*s>='A' && *s<='F') d = (unsigned long)(*s-'A'+10); + else break; + if (d >= base) break; + v = v*base + d; } return v; } diff --git a/fec/src/lower.c b/fec/src/lower.c index dbfc439..4953540 100644 --- a/fec/src/lower.c +++ b/fec/src/lower.c @@ -360,20 +360,22 @@ long literal_value(FeNode *n) return (long)(unsigned char)s[1]; } if (*s == '-') { neg = 1; ++s; } - if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { - s += 2; - for (; *s; ++s) { - int d = *s >= '0' && *s <= '9' ? *s - '0' : - *s >= 'a' && *s <= 'f' ? *s - 'a' + 10 : - *s >= 'A' && *s <= 'F' ? *s - 'A' + 10 : -1; - if (d < 0) { if (*s == '_') continue; break; } - v = v * 16 + d; - } - } else { + { + /* SPEC 3 spells four radices. Reading `0b1010` as decimal stops at the + `b` and answers zero, which is a number and so goes unnoticed. */ + int base = 10; + if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) { base = 16; s += 2; } + else if (s[0] == '0' && (s[1] == 'b' || s[1] == 'B')) { base = 2; s += 2; } + else if (s[0] == '0' && (s[1] == 'o' || s[1] == 'O')) { base = 8; s += 2; } for (; *s; ++s) { + int d; if (*s == '_') continue; - if (*s < '0' || *s > '9') break; - v = v * 10 + (*s - '0'); + if (*s >= '0' && *s <= '9') d = *s - '0'; + else if (*s >= 'a' && *s <= 'f') d = *s - 'a' + 10; + else if (*s >= 'A' && *s <= 'F') d = *s - 'A' + 10; + else break; + if (d >= base) break; + v = v * base + d; } } return neg ? -v : v; diff --git a/fec/src/parser.c b/fec/src/parser.c index 836babf..23224ba 100644 --- a/fec/src/parser.c +++ b/fec/src/parser.c @@ -71,6 +71,14 @@ static FeNode *type(FeParser *p) error(p,"expected type"); next(p); return fe_node(p->ast,FE_N_TYPE,t.loc,"error",5); } +/* Is this binary node one of the six comparisons? They share a precedence + level and SPEC 6.2 forbids chaining them. The operator is the node's text. */ +static int is_comparison(const char *op) +{ + if(!op) return 0; + return !strcmp(op,"==")||!strcmp(op,"!=")||!strcmp(op,"<")|| + !strcmp(op,"<=")||!strcmp(op,">")||!strcmp(op,">="); +} static int precedence(FeTokKind k) { switch(k) { @@ -107,7 +115,7 @@ static FeNode *primary(FeParser *p) } return n; } - if(eat(p,FE_TOK_LPAREN)) { int old=p->forbid_struct_literal; p->forbid_struct_literal=0; n=expr(p,0); p->forbid_struct_literal=old; want(p,FE_TOK_RPAREN,"expected ')'"); return n; } + if(eat(p,FE_TOK_LPAREN)) { int old=p->forbid_struct_literal; p->forbid_struct_literal=0; n=expr(p,0); p->forbid_struct_literal=old; want(p,FE_TOK_RPAREN,"expected ')'"); if(n) n->flags|=FE_NODE_PAREN; return n; } if(eat(p,FE_TOK_AT)) { FeToken name=p->current; if(!is_name(p)){error(p,"expected builtin name after '@'");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"builtin",7);} next(p); n=toknode(p,FE_N_CALL,name); n->text=fe_arena_strdup(&p->ast->arena,name.begin-1,name.length+1); @@ -167,9 +175,17 @@ static FeNode *postfix(FeParser *p) static FeNode *expr(FeParser *p, int minprec) { FeToken t=p->current; FeNode *left,*n; int prec; - if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_TILDE)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); left=n; } + if(is(p,FE_TOK_MINUS)||is(p,FE_TOK_NOT)||is(p,FE_TOK_TILDE)||is(p,FE_TOK_XOR)||is(p,FE_TOK_AND)||is(p,FE_TOK_STAR)||is(p,FE_TOK_TRY)) { next(p); n=toknode(p,FE_N_UNARY,t); if(t.kind==FE_TOK_AND && eat(p,FE_TOK_MUT)) n->text=fe_arena_strdup(&p->ast->arena,"&mut",4); n->a=expr(p,11); + if(n->a && n->a->kind==FE_N_TYPE && n->a->b && + !(n->a->flags & FE_NODE_PAREN)) + error(p,"parenthesise: '-x as T' is read as -(x as T)"); + left=n; } else left=postfix(p); - for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break;next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; } + for(;;) { t=p->current;prec=precedence(t.kind);if(prec<=minprec)break; + if(prec==4 && left && left->kind==FE_N_BINARY && + !(left->flags & FE_NODE_PAREN) && is_comparison(left->text)) + error(p,"comparisons do not chain; write 'a < b and b < c'"); + next(p);n=toknode(p,FE_N_BINARY,t);n->a=left;if(t.kind==FE_TOK_CATCH && eat(p,FE_TOK_OR)){if(is_name(p))n->b=toknode(p,FE_N_IDENT,p->current),next(p);else error(p,"expected catch binding");want(p,FE_TOK_OR,"expected '|' after catch binding");n->c=block(p);}else n->b=expr(p,prec);left=n; } return left; } @@ -208,7 +224,16 @@ static FeNode *fn_decl(FeParser *p, int pub, int external, int interrupt, int in FeToken t=p->current, name; FeNode *n; (void)interrupt; (void)interrupt_safe; want(p,FE_TOK_FN,"expected 'fn'"); if(!is_name(p)){error(p,"expected function name");return fe_node(p->ast,FE_N_ERROR_NODE,t.loc,"fn",2);} - name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; if(external) n->flags|=FE_NODE_EXTERN; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); if(eat(p,FE_TOK_SEMI)) return n; n->c=block(p); return n; + name=p->current; n=toknode(p,FE_N_FN,t); if(pub) n->flags|=FE_NODE_PUB; if(external) n->flags|=FE_NODE_EXTERN; n->text=fe_arena_strdup(&p->ast->arena,name.begin,name.length); next(p); n->a=params(p); if(eat(p,FE_TOK_ARROW)) n->b=type(p); + if(eat(p,FE_TOK_SEMI)) { + /* A body-less function is a promise that someone else defines it, and + `extern` is how that promise is made. Without it the name is + mangled into this unit and nothing anywhere defines it. */ + if(!external) error(p,"a function without a body must be extern"); + return n; + } + if(external) error(p,"an extern function has no body"); + n->c=block(p); return n; } static FeNode *field(FeParser *p, int pub) { @@ -220,15 +245,25 @@ static FeNode *decl(FeParser *p) int pub=0, external=0, interrupt=0, interrupt_safe=0, shared=0, atomic=0; FeToken t=p->current; FeNode *n; FeTokKind before; (void)shared; (void)atomic; if(eat(p,FE_TOK_PUB)) pub=1; - if(eat(p,FE_TOK_EXTERN)) { external=1; if(is(p,FE_TOK_STRING)) next(p); } + if(eat(p,FE_TOK_EXTERN)) { + external=1; + if(!is(p,FE_TOK_STRING)) error(p,"extern requires an ABI string"); + else { + /* The token keeps its quotes, so "c" is four characters. */ + FeToken abi=p->current; + if(abi.length!=3 || abi.begin[1]!='c') + error(p,"the only ABI is \"c\""); + next(p); + } + } if(eat(p,FE_TOK_INTERRUPT)) interrupt=1; if(eat(p,FE_TOK_INTERRUPT_SAFE)) interrupt_safe=1; if(!is(p,FE_TOK_PACKED)) t=p->current; if(is(p,FE_TOK_FN)) return fn_decl(p,pub,external,interrupt,interrupt_safe); if(eat(p,FE_TOK_PACKED)) t=p->previous; if(eat(p,FE_TOK_STRUCT)) { n=toknode(p,FE_N_STRUCT,t);if(pub)n->flags|=FE_NODE_PUB;if(t.kind==FE_TOK_PACKED)n->flags|=FE_NODE_PACKED;if(!is_name(p)){error(p,"expected struct name");return n;}next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in struct");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){int mpub=eat(p,FE_TOK_PUB);if(is(p,FE_TOK_FN))fe_node_add(n,fn_decl(p,mpub,0,0,0));else fe_node_add(n,field(p,mpub));}want(p,FE_TOK_RBRACE,"expected '}' after struct");return n; } - if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in enum");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } - if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");if(is(p,FE_TOK_INT)){v->a=toknode(p,FE_N_LITERAL,p->current);next(p);}else error(p,"an error code must be an integer literal");if(!is(p,FE_TOK_COMMA)&&!is(p,FE_TOK_RBRACE)){error(p,"an error code must be an integer literal");recover(p);break;}want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } + if(eat(p,FE_TOK_ENUM)) { n=toknode(p,FE_N_ENUM,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected enum name");if(eat(p,FE_TOK_LPAREN)){n->a=fe_node(p->ast,FE_N_BLOCK,p->current.loc,"generics",8);while(!is(p,FE_TOK_RPAREN)&&!is(p,FE_TOK_EOF)){fe_node_add(n->a,type(p));if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RPAREN,"expected ')' after generic parameters");}want(p,FE_TOK_LBRACE,"expected '{' in enum");if(is(p,FE_TOK_RBRACE))error(p,"an enum needs at least one variant");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected variant name");recover(p);break;}if(eat(p,FE_TOK_LPAREN)){v->a=type(p);want(p,FE_TOK_RPAREN,"expected ')' in variant");}else if(eat(p,FE_TOK_LBRACE)){while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF))fe_node_add(v,field(p,1));want(p,FE_TOK_RBRACE,"expected '}' in variant");}fe_node_add(n,v);if(!eat(p,FE_TOK_COMMA))break;}want(p,FE_TOK_RBRACE,"expected '}' after enum");return n; } + if(eat(p,FE_TOK_ERROR_KW)) { n=toknode(p,FE_N_ERROR_DECL,t);if(pub)n->flags|=FE_NODE_PUB;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected error name");want(p,FE_TOK_LBRACE,"expected '{' in error declaration");if(is(p,FE_TOK_RBRACE))error(p,"an error declaration needs at least one member");while(!is(p,FE_TOK_RBRACE)&&!is(p,FE_TOK_EOF)){FeNode *v=toknode(p,FE_N_VARIANT,p->current);if(is_name(p))next(p);else{error(p,"expected error member");recover(p);break;}want(p,FE_TOK_EQ,"expected '=' in error member");if(is(p,FE_TOK_INT)){v->a=toknode(p,FE_N_LITERAL,p->current);next(p);}else error(p,"an error code must be an integer literal");if(!is(p,FE_TOK_COMMA)&&!is(p,FE_TOK_RBRACE)){error(p,"an error code must be an integer literal");recover(p);break;}want(p,FE_TOK_COMMA,"expected ',' in error declaration");fe_node_add(n,v);}want(p,FE_TOK_RBRACE,"expected '}' after error");return n; } if(eat(p,FE_TOK_SHARED)) { shared=1; if(eat(p,FE_TOK_ATOMIC)) atomic=1; if(!is(p,FE_TOK_VAR)) error(p,"expected 'var' after shared"); } if(is(p,FE_TOK_CONST)||is(p,FE_TOK_STATIC)||is(p,FE_TOK_VAR)) { FeTokKind kk=p->current.kind;next(p);n=toknode(p,kk==FE_TOK_CONST?FE_N_CONST:FE_N_GLOBAL,t);if(pub)n->flags|=FE_NODE_PUB;if(kk==FE_TOK_STATIC)n->flags|=FE_NODE_STATIC;if(shared)n->flags|=FE_NODE_SHARED;if(is_name(p)){next(p);n->text=fe_arena_strdup(&p->ast->arena,p->previous.begin,p->previous.length);}else error(p,"expected declaration name");if(eat(p,FE_TOK_COLON))n->a=type(p);else if(kk!=FE_TOK_CONST)error(p,"a global declaration requires an explicit type");want(p,FE_TOK_EQ,"expected '=' in declaration");n->b=expr(p,0);want(p,FE_TOK_SEMI,"expected ';' after declaration");return n; } error(p,"expected declaration"); before=p->current.kind; recover(p); diff --git a/fec/tests/exec/radix.fe b/fec/tests/exec/radix.fe new file mode 100644 index 0000000..378e5e1 --- /dev/null +++ b/fec/tests/exec/radix.fe @@ -0,0 +1,29 @@ +// EXIT:0 +// OUTPUT:bin 10 255 0 +// OUTPUT:oct 15 511 8 +// OUTPUT:hex 255 4095 16 +// OUTPUT:dec 1000 1000000 +// OUTPUT:same yes yes +unit radix; + +import std.io; + +// SPEC §3 spells four radices. Reading `0b1010` as decimal stops at the `b` +// and answers zero -- which is a number, so nothing looks wrong until the +// program does the wrong thing. + +fn main() -> i32 { + @print("bin {} {} {}\n", 0b1010, 0b11111111, 0b0); + @print("oct {} {} {}\n", 0o17, 0o777, 0o10); + @print("hex {} {} {}\n", 0xFF, 0xfff, 0x10); + @print("dec {} {}\n", 1_000, 1_000_000); + + // The same number four ways. + @print("same {} {}\n", yesno(0b1111 == 0o17), yesno(0o17 == 0xF)); + return 0; +} + +fn yesno(b: bool) -> []u8 { + if b { return "yes"; } + return "no"; +} diff --git a/fec/tests/parse/bademen.fe b/fec/tests/parse/bademen.fe new file mode 100644 index 0000000..9c72977 --- /dev/null +++ b/fec/tests/parse/bademen.fe @@ -0,0 +1,3 @@ +// ERROR:3:at least one variant +unit bademen; +enum E { } diff --git a/fec/tests/parse/bademer.fe b/fec/tests/parse/bademer.fe new file mode 100644 index 0000000..a690859 --- /dev/null +++ b/fec/tests/parse/bademer.fe @@ -0,0 +1,3 @@ +// ERROR:3:at least one member +unit bademer; +error E { } diff --git a/fec/tests/types/badasm.fe b/fec/tests/types/badasm.fe new file mode 100644 index 0000000..9cb70e4 --- /dev/null +++ b/fec/tests/types/badasm.fe @@ -0,0 +1,3 @@ +// ERROR:3:unsafe block +unit badasm; +fn f() -> void { asm { "nop" } return; } diff --git a/fec/tests/types/badchain.fe b/fec/tests/types/badchain.fe new file mode 100644 index 0000000..9aea447 --- /dev/null +++ b/fec/tests/types/badchain.fe @@ -0,0 +1,3 @@ +// ERROR:3:do not chain +unit badchain; +fn f() -> bool { return true == false == true; } diff --git a/fec/tests/types/badcini.fe b/fec/tests/types/badcini.fe new file mode 100644 index 0000000..c52a9dd --- /dev/null +++ b/fec/tests/types/badcini.fe @@ -0,0 +1,4 @@ +// ERROR:4:known at compile time +unit badcini; +fn r() -> i32 { return 1; } +const A: i32 = r(); diff --git a/fec/tests/types/badexab.fe b/fec/tests/types/badexab.fe new file mode 100644 index 0000000..1ab2d2a --- /dev/null +++ b/fec/tests/types/badexab.fe @@ -0,0 +1,3 @@ +// ERROR:3:only ABI +unit badexab; +extern "stdcall" fn f(); diff --git a/fec/tests/types/badexbd.fe b/fec/tests/types/badexbd.fe new file mode 100644 index 0000000..06252f5 --- /dev/null +++ b/fec/tests/types/badexbd.fe @@ -0,0 +1,3 @@ +// ERROR:3:no body +unit badexbd; +extern "c" fn f() -> i32 { return 1; } diff --git a/fec/tests/types/badexns.fe b/fec/tests/types/badexns.fe new file mode 100644 index 0000000..b51ce2f --- /dev/null +++ b/fec/tests/types/badexns.fe @@ -0,0 +1,3 @@ +// ERROR:3:ABI string +unit badexns; +extern fn f(); diff --git a/fec/tests/types/badfnsm.fe b/fec/tests/types/badfnsm.fe new file mode 100644 index 0000000..cd3c37a --- /dev/null +++ b/fec/tests/types/badfnsm.fe @@ -0,0 +1,3 @@ +// ERROR:4:must be extern +unit badfnsm; +fn f() -> i32; diff --git a/fec/tests/types/badsini.fe b/fec/tests/types/badsini.fe new file mode 100644 index 0000000..c585800 --- /dev/null +++ b/fec/tests/types/badsini.fe @@ -0,0 +1,4 @@ +// ERROR:4:known at compile time +unit badsini; +fn r() -> i32 { return 1; } +static A: i32 = r(); diff --git a/fec/tests/types/badunas.fe b/fec/tests/types/badunas.fe new file mode 100644 index 0000000..849ef40 --- /dev/null +++ b/fec/tests/types/badunas.fe @@ -0,0 +1,3 @@ +// ERROR:3:parenthesise +unit badunas; +fn f(x: i32) -> u32 { return -x as u32; } diff --git a/fec/tests/types/okglobin.fe b/fec/tests/types/okglobin.fe new file mode 100644 index 0000000..9549013 --- /dev/null +++ b/fec/tests/types/okglobin.fe @@ -0,0 +1,23 @@ +unit okglobin; + +// 거부되는 것들의 짝. 전역 초기값은 컴파일 시점에 알 수 있어야 하고, +// extern 은 ABI 를 대고 본문이 없으며, 비교는 괄호로 묶으면 이어 쓸 수 있고, +// 단항 뒤의 as 는 괄호가 어느 쪽인지 말해주면 되고, asm 은 unsafe 안이면 된다. +import std.sys; + +const A: i32 = 1; +const B: i32 = A + 2; +const S: str = "hi"; +static C: i32 = -A; +var D: u32 = 0xFF; + +enum E { One, Two, } +error Er { Bad = 1, } + +extern "c" fn fe_rt_allocs() -> i32; + +fn f(x: i32) -> u32 { return (-x) as u32; } +fn g(x: i32) -> u32 { return -(x as u32); } +fn h(a: i32, b: i32, c: i32) -> bool { return a < b and b < c; } +fn i(a: i32, b: i32) -> bool { return (a == b) == true; } +fn j() -> void { unsafe { asm { "nop" } } return; }