aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-07-26 15:45:40 +0000
committer_ov4 <ov4@ov4.org>2026-07-26 15:45:40 +0000
commit344206d57e834b2394cbe2ec0df50b04a8b3c171 (patch)
tree0eb282bc9878bf990496a5d840ca8492b3cfc508
parentd1c47a328785ece9a8ffd9bbca3138818b8203b3 (diff)
feat: implement CIE 9.1 text file handling (OPENFILE/READFILE/WRITEFILE/CLOSEFILE/EOF)
Front end: OPENFILE/READFILE/WRITEFILE/CLOSEFILE plus reserved mode keywords READ/WRITE/APPEND (RANDOM reserved with a dedicated not-implemented error); statement nodes in new FileAST.h, parsing in new ParserFile.cc; EOF joins the Builtins table as the 12th builtin and CallExprAST now carries a line number for its runtime diagnostics. Runtime: new FileHandler (StringHandler-shaped leaf) lazily synthesizes internal __cps_file_* helper functions and a 16-slot open-file table keyed by filename content (strcmp — STRING '=' is pointer identity). EOF is an fgetc/ungetc peek; READFILE grows a malloc/realloc line buffer and strips a trailing CR; fopen uses text modes so mingw-w64 line translation works. File-free programs get byte-identical IR. All misuse (not open, wrong mode, reopen, fopen failure, read past EOF, table full) dies with the existing "[Fatal] line N:" pattern via a new RuntimeCheck::emitErrorAndExit overload taking runtime printf args. Also fixes a pre-existing ArithmeticHandler hole surfaced by adversarial review: mixed-width integer operands (i1 BOOLEAN / i8 CHAR vs i64) now widen via ZExt instead of emitting invalid IR, and remaining type mismatches get a clean diagnostic; -EOF(f) and -IS_NUM(x) compile. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-rw-r--r--AGENTS/ARCH.md5
-rw-r--r--AGENTS/CONVENTIONS.md26
-rw-r--r--AGENTS/EXTENDING.md3
-rw-r--r--AGENTS/PITFALLS.md23
-rw-r--r--AGENTS/SUBSYSTEMS.md27
-rw-r--r--CMakeLists.txt4
-rw-r--r--README.md5
-rw-r--r--include/cps/CodeGen.h3
-rw-r--r--include/cps/FileAST.h63
-rw-r--r--include/cps/FileHandler.h75
-rw-r--r--include/cps/FileMode.h11
-rw-r--r--include/cps/FunctionAST.h6
-rw-r--r--include/cps/Lexer.h11
-rw-r--r--include/cps/Parser.h5
-rw-r--r--include/cps/RuntimeCheck.h7
-rw-r--r--lib/CodeGen/ArithmeticHandler.cc60
-rw-r--r--lib/CodeGen/CodeGen.cc85
-rw-r--r--lib/CodeGen/FileHandler.cc452
-rw-r--r--lib/CodeGen/FunctionGen.cc2
-rw-r--r--lib/CodeGen/RuntimeCheck.cc8
-rw-r--r--lib/Lexer/Lexer.cc9
-rw-r--r--lib/Parser/Parser.cc15
-rw-r--r--lib/Parser/ParserFile.cc92
23 files changed, 951 insertions, 46 deletions
diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md
index cad017f..cbf4b65 100644
--- a/AGENTS/ARCH.md
+++ b/AGENTS/ARCH.md
@@ -47,12 +47,13 @@ untested.)
| `lib/Lexer/Lexer.cc` | Entire lexer: one function, `Lexer::gettok()` |
| `lib/Parser/Parser.cc` | Statements, expressions, precedence table |
| `lib/Parser/ParserFunction.cc` | FUNCTION/PROCEDURE/CALL/RETURN parsing |
+| `lib/Parser/ParserFile.cc` | OPENFILE/READFILE/WRITEFILE/CLOSEFILE parsing |
| `lib/CodeGen/CodeGen.cc` | Hub: dispatch, coercion, control flow, INPUT/OUTPUT, main() |
| `lib/CodeGen/*Handler.cc`, `FunctionGen.cc`, `RuntimeCheck.cc`, `TypeSystem.cc` | Helpers owned by CodeGen |
| `CMakeLists.txt` | 3 static libs (CPSLexer, CPSParser, CPSCodeGen) + `cpsc`. **New .cc files must be added to the source lists here** |
-AST nodes live in `include/cps/AST.h` (+ `FunctionAST.h`); they are pure data with
-virtual destructors only — no codegen/visitor methods.
+AST nodes live in `include/cps/AST.h` (+ `FunctionAST.h`, `FileAST.h`); they are pure
+data with virtual destructors only — no codegen/visitor methods.
## Detail docs
diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md
index 333af36..03a9452 100644
--- a/AGENTS/CONVENTIONS.md
+++ b/AGENTS/CONVENTIONS.md
@@ -31,14 +31,16 @@ pseudocode types *backwards* from LLVM types (`getExprTypeInfo`'s fallback,
`ArithmeticHandler`'s `isIntegerTy(64)` tests), so these widths are ABI.
**Strings are malloc'd and never freed — by design.** Every string-producing operation
-(`&`, MID/LEFT/RIGHT/LCASE/UCASE, NUM_TO_STR, CHR, CHAR/number→STRING coercion) mallocs
-a fresh buffer; string literals and BOOLEAN→STRING coercion instead return pointers to
-shared module globals, so a STRING value may alias a global. `free` is never declared or
-called. Don't "fix" leaks piecemeal; it's a global design decision. Heap-string
-operations live in `StringHandler` (including `&` concatenation, `emitConcat`) and
-`StringConversionHandler` (NUM_TO_STR/STR_TO_NUM buffers); `CodeGen` also keeps its own
-`MallocFunc` for the char→STRING coercion and INPUT STRING buffers — declared in
-`SetupExternalFunctions`, never fetched with a bare `getFunction("malloc")`.
+(`&`, MID/LEFT/RIGHT/LCASE/UCASE, NUM_TO_STR, CHR, READFILE, CHAR/number→STRING
+coercion) mallocs a fresh buffer; string literals and BOOLEAN→STRING coercion instead
+return pointers to shared module globals, so a STRING value may alias a global. `free`
+is never declared or called. Don't "fix" leaks piecemeal; it's a global design decision.
+Heap-string operations live in `StringHandler` (including `&` concatenation,
+`emitConcat`), `StringConversionHandler` (NUM_TO_STR/STR_TO_NUM buffers) and
+`FileHandler` (READFILE line buffers — its `__cps_file_read` helper owns the project's
+only `realloc`); `CodeGen` also keeps its own `MallocFunc` for the char→STRING coercion
+and INPUT STRING buffers — declared in `SetupExternalFunctions`, never fetched with a
+bare `getFunction("malloc")`.
**One flat symbol namespace, one table.** `Symbols` (name → `SymbolInfo{Storage,
TypeName, IsArray}`) in CodeGen is the only symbol table; register through
@@ -72,10 +74,12 @@ no registered signature is a compile error. Function names get no mangling, so
`emitPrototype` rejects names that collide with runtime/libc symbols
(`isReservedRuntimeName`) or with builtins.
-**Builtins are plain identifiers backed by one table.** All 11 builtins
-(LENGTH/MID/RIGHT/LEFT/LCASE/UCASE/ASC/CHR/IS_NUM/NUM_TO_STR/STR_TO_NUM) parse as
+**Builtins are plain identifiers backed by one table.** All 12 builtins
+(LENGTH/MID/RIGHT/LEFT/LCASE/UCASE/ASC/CHR/IS_NUM/NUM_TO_STR/STR_TO_NUM/EOF) parse as
ordinary calls — no lexer/parser support, and the names are not reserved words. The
static `Builtins` table at the top of `CodeGen.cc` ({Name, Arity, ReturnTypeName}) is
the single source of truth: `emitExpr`'s `CallExprAST` branch checks arity against it
before dispatching to the per-builtin emit branches (still an if-chain, ahead of the
-user-function fallback), and `getExprTypeInfo` reads return types from it.
+user-function fallback), and `getExprTypeInfo` reads return types from it. EOF is the
+odd one out: its intercept consumes `CallExprAST::getLine()` (runtime diagnostics) and
+it is the only builtin that touches mutable runtime state and can exit(1) at run time.
diff --git a/AGENTS/EXTENDING.md b/AGENTS/EXTENDING.md
index f6cf1b7..07549e3 100644
--- a/AGENTS/EXTENDING.md
+++ b/AGENTS/EXTENDING.md
@@ -4,7 +4,8 @@ Step-by-step patterns for adding features. Read `CONVENTIONS.md` first — every
below assumes those invariants.
**New statement:** token in `Lexer.h` enum + keyword match in `Lexer.cc`'s if-chain →
-node class in `AST.h` (unique_ptr children, move ctor, raw-ptr getters) → `ParseXxx` in
+node class in `AST.h` or a feature header (`FunctionAST.h`/`FileAST.h` precedent;
+unique_ptr children, move ctor, raw-ptr getters) → `ParseXxx` in
`Parser.cc` + branch in `ParseStatementImpl` (block statements parse their body with
`ParseBlock({terminator tokens})`; comma-separated expression lists use
`ParseExprList(close, allowEmpty)`; add the new statement's leading keyword to
diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md
index 967d577..e24b4bc 100644
--- a/AGENTS/PITFALLS.md
+++ b/AGENTS/PITFALLS.md
@@ -30,11 +30,24 @@ test programs.
loop direction is decided at run time from the STEP value's sign; STEP is evaluated
exactly once, before the loop. (The TO expression is still re-evaluated every
iteration.)
+- **File handling (§9.1) sharp edges.** `READ`/`WRITE`/`APPEND`/`RANDOM` are reserved
+ keywords now — `DECLARE READ : INTEGER` no longer compiles. `WRITEFILE f, a, b` takes
+ exactly two operands (the stray `, b` is a parse error, like multi-expression OUTPUT).
+ READFILE's target must be a plain STRING variable — no CHAR, no `arr[i]` (mirrors
+ INPUT). The CIE guide's empty-line test `IF LineOfText = ""` silently fails here
+ because STRING `=` is pointer identity — use `LENGTH(LineOfText) = 0`. At most 16
+ files open at once. All runtime misuse (not open, wrong mode, reopen, fopen failure,
+ READFILE past EOF) is a `[Fatal] line %d:` exit(1), by design — guard reads with
+ `WHILE NOT EOF(...)`. Caveat: the open-file table is keyed by the filename *string*
+ (per CIE semantics), so two spellings of one OS path (`"a.txt"` vs `"./a.txt"`) evade
+ the already-open check and get two independent buffered streams — reads through the
+ alias can see stale/empty data instead of a Fatal.
## Not implemented (9618 features)
-CASE OF, CONSTANT, TYPE/ENDTYPE (records/enums/pointers), file handling
-(OPENFILE/READFILE/WRITEFILE/CLOSEFILE/EOF), DATE, multi-expression OUTPUT, INPUT into
-array elements, string escape sequences. `README.md` lists `type` and `file` as the
-intended next work. No tests and no CI exist; verify changes by compiling sample
-programs end-to-end (command sequence in `ARCH.md`).
+CASE OF, CONSTANT, TYPE/ENDTYPE (records/enums/pointers), random files
+(OPENFILE...FOR RANDOM/SEEK/GETRECORD/PUTRECORD — blocked on TYPE; the RANDOM keyword
+is reserved and gives a dedicated parse error), DATE, multi-expression OUTPUT, INPUT
+into array elements, string escape sequences. `README.md` lists `type` as the intended
+next work. No tests and no CI exist; verify changes by compiling sample programs
+end-to-end (command sequence in `ARCH.md`).
diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md
index f26cddc..6a89e2b 100644
--- a/AGENTS/SUBSYSTEMS.md
+++ b/AGENTS/SUBSYSTEMS.md
@@ -1,6 +1,6 @@
# Subsystem notes
-Deep-dives on the three CodeGen helper subsystems. (Invariants: `CONVENTIONS.md`;
+Deep-dives on the CodeGen helper subsystems. (Invariants: `CONVENTIONS.md`;
known bugs: `PITFALLS.md`.)
**Arrays** (`ArrayHandler`): a DECLAREd array is one malloc'd flat buffer (memset to
@@ -27,6 +27,25 @@ calling a name with no signature is a compile error (no extern is fabricated). B
emission is inverted: CodeGen injects a `StmtEmitter` callback, and must restore its
own insert point afterwards.
-**Runtime checks** (`RuntimeCheck`): only array bounds checking is actually wired up.
-`emitDivZeroCheck` exists but has **zero call sites** — division by zero (for `/`, DIV
-and MOD) is currently unchecked; the TODO sits in `ArithmeticHandler.cc`.
+**Runtime checks** (`RuntimeCheck`): only array bounds checking and the file checks are
+actually wired up. `emitDivZeroCheck` exists but has **zero call sites** — division by
+zero (for `/`, DIV and MOD) is currently unchecked; the TODO sits in
+`ArithmeticHandler.cc`. The public `emitErrorAndExit(Cond, Msg, ArrayRef<Value*>)`
+overload exists for failure sites whose printf arguments (line, filename) are runtime
+values — FileHandler's helpers use it; the private compile-time-line form delegates to
+it.
+
+**Files** (`FileHandler`): text-file runtime for §9.1. StringHandler-shaped leaf —
+holds Context/Builder/Module/RuntimeCheck refs only, never sees CodeGen, AST nodes or
+the symbol table; `CodeGen::emitStmt`'s four file branches (and the EOF builtin
+intercept) evaluate/coerce operands and hand over `llvm::Value*`s plus the statement
+line. On first file use, `ensureRuntime()` synthesizes the whole runtime once — the
+only module-level helper functions in the project: internal-linkage `__cps_file_find/
+open/read/write/close/eof` plus a 16-slot open-file table (`__cps_file_names/handles/
+modes` parallel globals; mode byte 0 = free). Files are keyed by filename *content*
+(strcmp scan) because STRING `=` is pointer identity; the name pointer is stored as-is
+(strings are immutable and never freed). EOF is an fgetc/ungetc peek; READFILE grows a
+malloc/realloc buffer per line and strips one trailing `\r`; fopen modes are text
+`"r"/"w"/"a"` (Windows CRT translates CRLF). File-free programs get byte-identical IR —
+nothing is declared or emitted eagerly. Helper bodies are built with the *shared*
+Builder (RuntimeCheck emits through the same one), saving/restoring the insert block.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4f80d0a..4a3c29d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -13,9 +13,10 @@ set(CMAKE_CXX_STANDARD 17)
include_directories(include)
add_library(CPSLexer lib/Lexer/Lexer.cc)
-add_library(CPSParser
+add_library(CPSParser
lib/Parser/Parser.cc
lib/Parser/ParserFunction.cc
+ lib/Parser/ParserFile.cc
)
add_library(CPSCodeGen
@@ -27,6 +28,7 @@ add_library(CPSCodeGen
lib/CodeGen/StringHandler.cc
lib/CodeGen/StringConversionHandler.cc
lib/CodeGen/TypeSystem.cc
+ lib/CodeGen/FileHandler.cc
)
llvm_map_components_to_libnames(llvm_libs core support native irreader)
diff --git a/README.md b/README.md
index 8d973ec..79250d2 100644
--- a/README.md
+++ b/README.md
@@ -15,13 +15,16 @@ IDK WHAT HAPPENDED IT IS 100% AI-GENERATED DO NOT OPEN ANY ISSUES JUST CLONE AND
- cache-friendly array (perhaps)
- dynamic string handling, with full implementation of all CIE string functions, with high security
- native support for BYREF/BYVAL using pointers
+- text file handling (OPENFILE/READFILE/WRITEFILE/CLOSEFILE/EOF) with runtime misuse
+ checking (wrong mode, not open, read past EOF, ... all die with a clean `[Fatal]`
+ message instead of corrupting anything)
- ultra-fast compiling & executing
## TODO
- search for `TODO` in my code
- type
-- file
+- random files (OPENFILE FOR RANDOM / SEEK / GETRECORD / PUTRECORD) — needs `type` first
## Dependencies
diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h
index 140d1f5..43d3a32 100644
--- a/include/cps/CodeGen.h
+++ b/include/cps/CodeGen.h
@@ -18,6 +18,7 @@
namespace cps {
class ArrayHandler;
+class FileHandler;
class CodeGen {
std::unique_ptr<llvm::LLVMContext> TheContext;
@@ -33,6 +34,7 @@ class CodeGen {
std::unique_ptr<ArithmeticHandler> ArithHandler;
std::unique_ptr<StringHandler> StrHandler;
std::unique_ptr<StringConversionHandler> StrConvHandler;
+ std::unique_ptr<FileHandler> Files;
llvm::FunctionCallee PrintfFunc;
llvm::FunctionCallee ScanfFunc;
@@ -59,6 +61,7 @@ class CodeGen {
void registerSymbol(const std::string &Name, llvm::Value *Storage, const std::string &TypeName, bool IsArray = false);
const SymbolInfo *getSymbolInfo(const std::string &Name) const;
+ bool isWholeArrayVar(ExprAST *Expr) const;
const TypeInfo *getExprTypeInfo(ExprAST *Expr) const;
llvm::Value *coerceValueToType(llvm::Value *Val, const TypeInfo *TargetInfo);
bool marshalCallArgs(const std::string &Callee,
diff --git a/include/cps/FileAST.h b/include/cps/FileAST.h
new file mode 100644
index 0000000..04bd06a
--- /dev/null
+++ b/include/cps/FileAST.h
@@ -0,0 +1,63 @@
+#pragma once
+#include "cps/AST.h"
+#include "cps/FileMode.h"
+#include <memory>
+#include <string>
+
+namespace cps {
+
+class OpenFileStmtAST : public StmtAST {
+ std::unique_ptr<ExprAST> FileName;
+ FileMode Mode;
+ int Line;
+
+public:
+ OpenFileStmtAST(std::unique_ptr<ExprAST> FileName, FileMode Mode, int Line)
+ : FileName(std::move(FileName)), Mode(Mode), Line(Line) {}
+
+ ExprAST *getFileName() const { return FileName.get(); }
+ FileMode getMode() const { return Mode; }
+ int getLine() const { return Line; }
+};
+
+class ReadFileStmtAST : public StmtAST {
+ std::unique_ptr<ExprAST> FileName;
+ std::string VarName;
+ int Line;
+
+public:
+ ReadFileStmtAST(std::unique_ptr<ExprAST> FileName, const std::string &VarName, int Line)
+ : FileName(std::move(FileName)), VarName(VarName), Line(Line) {}
+
+ ExprAST *getFileName() const { return FileName.get(); }
+ const std::string &getVarName() const { return VarName; }
+ int getLine() const { return Line; }
+};
+
+class WriteFileStmtAST : public StmtAST {
+ std::unique_ptr<ExprAST> FileName;
+ std::unique_ptr<ExprAST> Data;
+ int Line;
+
+public:
+ WriteFileStmtAST(std::unique_ptr<ExprAST> FileName, std::unique_ptr<ExprAST> Data, int Line)
+ : FileName(std::move(FileName)), Data(std::move(Data)), Line(Line) {}
+
+ ExprAST *getFileName() const { return FileName.get(); }
+ ExprAST *getData() const { return Data.get(); }
+ int getLine() const { return Line; }
+};
+
+class CloseFileStmtAST : public StmtAST {
+ std::unique_ptr<ExprAST> FileName;
+ int Line;
+
+public:
+ CloseFileStmtAST(std::unique_ptr<ExprAST> FileName, int Line)
+ : FileName(std::move(FileName)), Line(Line) {}
+
+ ExprAST *getFileName() const { return FileName.get(); }
+ int getLine() const { return Line; }
+};
+
+} // namespace cps
diff --git a/include/cps/FileHandler.h b/include/cps/FileHandler.h
new file mode 100644
index 0000000..aaad6a0
--- /dev/null
+++ b/include/cps/FileHandler.h
@@ -0,0 +1,75 @@
+#pragma once
+#include "cps/FileMode.h"
+#include "llvm/IR/IRBuilder.h"
+#include "llvm/IR/LLVMContext.h"
+#include "llvm/IR/Module.h"
+
+namespace cps {
+
+class RuntimeCheck;
+
+// Emits the text-file runtime: a fixed-size open-file table plus internal
+// __cps_file_* helper functions, synthesized once on first use so programs
+// without file statements keep their IR unchanged. The table is keyed by
+// filename *content* (strcmp) — pseudocode STRING equality is pointer
+// identity, so the same file may be named by many different buffers.
+// Leaf module: methods only take and return llvm::Value*s; expression
+// evaluation, type coercion and the symbol table stay in CodeGen.
+class FileHandler {
+ llvm::LLVMContext &Context;
+ llvm::IRBuilder<> &Builder;
+ llvm::Module &Module;
+ RuntimeCheck &Checker;
+
+ static constexpr uint64_t TableSize = 16;
+ bool RuntimeEmitted = false;
+
+ // libc — declared lazily on first file use
+ llvm::FunctionCallee FopenFunc, FcloseFunc, FgetcFunc, UngetcFunc,
+ FputsFunc, FputcFunc, MallocFunc, ReallocFunc,
+ StrcmpFunc;
+
+ // open-file table
+ llvm::GlobalVariable *FileNames = nullptr; // [TableSize x ptr] STRING passed to OPENFILE
+ llvm::GlobalVariable *FileHandles = nullptr; // [TableSize x ptr] FILE*
+ llvm::GlobalVariable *FileModes = nullptr; // [TableSize x i8] 0 = free slot, else FileMode
+
+ llvm::Value *ModeReadStr = nullptr, *ModeWriteStr = nullptr, *ModeAppendStr = nullptr;
+ llvm::Value *ErrOpenFail = nullptr, *ErrAlreadyOpen = nullptr, *ErrTableFull = nullptr,
+ *ErrNotOpen = nullptr, *ErrNotRead = nullptr, *ErrNotWrite = nullptr,
+ *ErrPastEof = nullptr;
+
+ // synthesized helpers (internal linkage; pseudocode identifiers cannot
+ // start with '_', so these names are un-collidable)
+ llvm::Function *FindFn = nullptr, *OpenFn = nullptr, *ReadFn = nullptr,
+ *WriteFn = nullptr, *CloseFn = nullptr, *EofFn = nullptr;
+
+ void setupExternalFunctions();
+ void setupGlobals();
+ void ensureRuntime();
+
+ llvm::Function *createHelper(const char *Name, llvm::FunctionType *FT);
+ llvm::Value *emitSlotGEP(llvm::GlobalVariable *Table, llvm::Type *ElemTy, llvm::Value *Idx);
+ llvm::Value *emitLookupOrFail(llvm::Value *Name, llvm::Value *Line);
+ void buildFindFn();
+ void buildOpenFn();
+ void buildReadFn();
+ void buildWriteFn();
+ void buildCloseFn();
+ void buildEofFn();
+
+public:
+ FileHandler(llvm::LLVMContext &Ctx, llvm::IRBuilder<> &B, llvm::Module &M,
+ RuntimeCheck &RC);
+
+ // NameStr/DataStr are already-coerced STRING ptrs; Line is the statement's
+ // source line for runtime diagnostics. All methods null-check and bail;
+ // the caller (CodeGen) does the diagnostics.
+ void emitOpen(llvm::Value *NameStr, FileMode Mode, int Line);
+ llvm::Value *emitRead(llvm::Value *NameStr, int Line); // returns STRING ptr
+ void emitWrite(llvm::Value *NameStr, llvm::Value *DataStr, int Line);
+ void emitClose(llvm::Value *NameStr, int Line);
+ llvm::Value *emitEof(llvm::Value *NameStr, int Line); // returns i1
+};
+
+} // namespace cps
diff --git a/include/cps/FileMode.h b/include/cps/FileMode.h
new file mode 100644
index 0000000..b95e615
--- /dev/null
+++ b/include/cps/FileMode.h
@@ -0,0 +1,11 @@
+#pragma once
+#include <cstdint>
+
+namespace cps {
+
+// Shared between FileAST (parser side) and FileHandler (codegen side).
+// The numeric values are the mode byte stored in the runtime file table;
+// 0 is reserved to mean "free slot".
+enum class FileMode : uint8_t { Read = 1, Write = 2, Append = 3 };
+
+} // namespace cps
diff --git a/include/cps/FunctionAST.h b/include/cps/FunctionAST.h
index 5b4da6c..613079c 100644
--- a/include/cps/FunctionAST.h
+++ b/include/cps/FunctionAST.h
@@ -42,13 +42,15 @@ public:
class CallExprAST : public ExprAST {
std::string Callee;
std::vector<std::unique_ptr<ExprAST>> Args;
+ int Line;
public:
- CallExprAST(const std::string &Callee, std::vector<std::unique_ptr<ExprAST>> Args)
- : Callee(Callee), Args(std::move(Args)) {}
+ CallExprAST(const std::string &Callee, std::vector<std::unique_ptr<ExprAST>> Args, int Line)
+ : Callee(Callee), Args(std::move(Args)), Line(Line) {}
const std::string &getCallee() const { return Callee; }
const std::vector<std::unique_ptr<ExprAST>> &getArgs() const { return Args; }
+ int getLine() const { return Line; }
};
class CallStmtAST : public StmtAST {
diff --git a/include/cps/Lexer.h b/include/cps/Lexer.h
index 8984398..81fbd99 100644
--- a/include/cps/Lexer.h
+++ b/include/cps/Lexer.h
@@ -69,7 +69,16 @@ enum Token {
tok_endprocedure = -405,
tok_call = -406,
tok_byref = -407,
- tok_byval = -408
+ tok_byval = -408,
+
+ tok_openfile = -500,
+ tok_readfile = -501,
+ tok_writefile = -502,
+ tok_closefile = -503,
+ tok_read = -504,
+ tok_write = -505,
+ tok_append = -506,
+ tok_random = -507
};
class Lexer {
diff --git a/include/cps/Parser.h b/include/cps/Parser.h
index 7d19c77..0ecb7f9 100644
--- a/include/cps/Parser.h
+++ b/include/cps/Parser.h
@@ -49,6 +49,11 @@ class Parser {
std::unique_ptr<StmtAST> ParseReturnStmt();
std::optional<std::vector<std::tuple<std::string, std::string, bool>>> ParsePrototypeArgs();
+ std::unique_ptr<StmtAST> ParseOpenFile();
+ std::unique_ptr<StmtAST> ParseReadFile();
+ std::unique_ptr<StmtAST> ParseWriteFile();
+ std::unique_ptr<StmtAST> ParseCloseFile();
+
public:
Parser(Lexer &L);
std::vector<std::unique_ptr<StmtAST>> Parse();
diff --git a/include/cps/RuntimeCheck.h b/include/cps/RuntimeCheck.h
index 3c31f41..97f4765 100644
--- a/include/cps/RuntimeCheck.h
+++ b/include/cps/RuntimeCheck.h
@@ -23,6 +23,13 @@ public:
void emitDivZeroCheck(llvm::Value *Divisor, int Line);
void emitIndexCheck(llvm::Value *Index, llvm::Value *Lower, llvm::Value *Upper, int Line);
+
+ // Runtime-computed variant: FmtArgs are the printf varargs after Msg
+ // (e.g. {i32 line, ptr filename}). Needed when the failure site is inside
+ // a synthesized helper function, where the line number is an argument
+ // rather than a compile-time constant.
+ void emitErrorAndExit(llvm::Value *Condition, llvm::Value *Msg,
+ llvm::ArrayRef<llvm::Value *> FmtArgs);
};
}
diff --git a/lib/CodeGen/ArithmeticHandler.cc b/lib/CodeGen/ArithmeticHandler.cc
index bfee395..439e4b1 100644
--- a/lib/CodeGen/ArithmeticHandler.cc
+++ b/lib/CodeGen/ArithmeticHandler.cc
@@ -5,6 +5,16 @@
using namespace llvm;
using namespace cps;
+// Widen an i1 (BOOLEAN) or i8 (CHAR) operand to i64 with the same ZExt
+// coerceValueToType uses, so a mixed pair like EOF(f) - 1 cannot reach the
+// instruction builders with mismatched integer widths (invalid IR).
+static Value *widenSmallInt(IRBuilder<> &Builder, LLVMContext &Context, Value *V) {
+ Type *Ty = V->getType();
+ if (Ty->isIntegerTy() && Ty->getIntegerBitWidth() < 64)
+ return Builder.CreateZExt(V, Type::getInt64Ty(Context), "widetmp");
+ return V;
+}
+
Value *ArithmeticHandler::emitBinaryOp(int Op, Value *LHS, Value *RHS, int Line) {
bool LIsInt = LHS->getType()->isIntegerTy(64);
bool RIsInt = RHS->getType()->isIntegerTy(64);
@@ -12,11 +22,16 @@ Value *ArithmeticHandler::emitBinaryOp(int Op, Value *LHS, Value *RHS, int Line)
bool RIsDouble = RHS->getType()->isDoubleTy();
if (Op == '/') {
- Value *LVal = LHS;
- Value *RVal = RHS;
- if (LIsInt) LVal = Builder.CreateSIToFP(LHS, Type::getDoubleTy(Context));
- if (RIsInt) RVal = Builder.CreateSIToFP(RHS, Type::getDoubleTy(Context));
-
+ Value *LVal = widenSmallInt(Builder, Context, LHS);
+ Value *RVal = widenSmallInt(Builder, Context, RHS);
+ if (LVal->getType()->isIntegerTy(64)) LVal = Builder.CreateSIToFP(LVal, Type::getDoubleTy(Context));
+ if (RVal->getType()->isIntegerTy(64)) RVal = Builder.CreateSIToFP(RVal, Type::getDoubleTy(Context));
+ if (!LVal->getType()->isDoubleTy() || !RVal->getType()->isDoubleTy()) {
+ fprintf(stderr, "Error: '/' requires numeric operands (line %d).\n", Line);
+ HadError = true;
+ return nullptr;
+ }
+
// TODO: DIV divide by 0 runtime check
return Builder.CreateFDiv(LVal, RVal, "divtmp");
}
@@ -68,17 +83,32 @@ Value *ArithmeticHandler::emitBinaryOp(int Op, Value *LHS, Value *RHS, int Line)
case tok_ge: return Builder.CreateFCmpOGE(LVal, RVal, "sgetmp");
}
} else {
+ // Same-type pairs (i64/i64, i8/i8 CHAR, i1/i1, ptr/ptr STRING) go
+ // through untouched; mixed integer widths are widened, anything else
+ // mismatched is a diagnostic instead of invalid IR.
+ Value *L = LHS;
+ Value *R = RHS;
+ if (L->getType() != R->getType() &&
+ L->getType()->isIntegerTy() && R->getType()->isIntegerTy()) {
+ L = widenSmallInt(Builder, Context, L);
+ R = widenSmallInt(Builder, Context, R);
+ }
+ if (L->getType() != R->getType()) {
+ fprintf(stderr, "Error: Mismatched operand types for binary operator (line %d).\n", Line);
+ HadError = true;
+ return nullptr;
+ }
switch (Op) {
- case '+': return Builder.CreateAdd(LHS, RHS, "addtmp");
- case '-': return Builder.CreateSub(LHS, RHS, "subtmp");
- case '*': return Builder.CreateMul(LHS, RHS, "multmp");
-
- case tok_eq: return Builder.CreateICmpEQ(LHS, RHS, "eqtmp");
- case tok_ne: return Builder.CreateICmpNE(LHS, RHS, "netmp");
- case '<': return Builder.CreateICmpSLT(LHS, RHS, "slttmp");
- case '>': return Builder.CreateICmpSGT(LHS, RHS, "sgttmp");
- case tok_le: return Builder.CreateICmpSLE(LHS, RHS, "sletmp");
- case tok_ge: return Builder.CreateICmpSGE(LHS, RHS, "sgetmp");
+ case '+': return Builder.CreateAdd(L, R, "addtmp");
+ case '-': return Builder.CreateSub(L, R, "subtmp");
+ case '*': return Builder.CreateMul(L, R, "multmp");
+
+ case tok_eq: return Builder.CreateICmpEQ(L, R, "eqtmp");
+ case tok_ne: return Builder.CreateICmpNE(L, R, "netmp");
+ case '<': return Builder.CreateICmpSLT(L, R, "slttmp");
+ case '>': return Builder.CreateICmpSGT(L, R, "sgttmp");
+ case tok_le: return Builder.CreateICmpSLE(L, R, "sletmp");
+ case tok_ge: return Builder.CreateICmpSGE(L, R, "sgetmp");
}
}
diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc
index ffe10b1..ae82914 100644
--- a/lib/CodeGen/CodeGen.cc
+++ b/lib/CodeGen/CodeGen.cc
@@ -1,5 +1,7 @@
#include "cps/CodeGen.h"
#include "cps/ArrayHandler.h"
+#include "cps/FileAST.h"
+#include "cps/FileHandler.h"
#include "cps/Lexer.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/Verifier.h"
@@ -32,6 +34,7 @@ constexpr BuiltinInfo Builtins[] = {
{"IS_NUM", 1, "BOOLEAN"},
{"NUM_TO_STR", 1, "STRING"},
{"STR_TO_NUM", 1, "REAL"},
+ {"EOF", 1, "BOOLEAN"},
};
const BuiltinInfo *findBuiltin(const std::string &Name) {
@@ -84,6 +87,7 @@ CodeGen::CodeGen() {
ArithHandler = std::make_unique<ArithmeticHandler>(*TheContext, *Builder, HadError);
StrHandler = std::make_unique<StringHandler>(*TheContext, *Builder, *TheModule);
StrConvHandler = std::make_unique<StringConversionHandler>(*TheContext, *Builder, *TheModule);
+ Files = std::make_unique<FileHandler>(*TheContext, *Builder, *TheModule, *RuntimeChecker);
SetupExternalFunctions();
}
@@ -144,6 +148,16 @@ const SymbolInfo *CodeGen::getSymbolInfo(const std::string &Name) const {
return &It->second;
}
+// A whole-array variable reference evaluates to the raw buffer pointer, which
+// the pointer->STRING coercion would accept unchanged; file-statement operands
+// must reject it explicitly or garbage gets treated as a C string.
+bool CodeGen::isWholeArrayVar(ExprAST *Expr) const {
+ auto *Var = dynamic_cast<VariableExprAST*>(Expr);
+ if (!Var) return false;
+ const SymbolInfo *Info = getSymbolInfo(Var->getName());
+ return Info && Info->IsArray;
+}
+
const TypeInfo *CodeGen::resolveType(const std::string &TypeName) const {
return Types->resolve(TypeName);
}
@@ -590,6 +604,16 @@ Value *CodeGen::emitExpr(ExprAST *Expr) {
if (!Str) return nullptr;
return StrConvHandler->emitStrToNum(Str, true);
}
+ if (Name == "EOF") {
+ if (isWholeArrayVar(Call->getArgs()[0].get())) {
+ reportError("Cannot use a whole array as the EOF filename");
+ return nullptr;
+ }
+ Value *FileName = coerceValueToType(emitExpr(Call->getArgs()[0].get()),
+ resolveType("STRING"));
+ if (!FileName) return nullptr;
+ return Files->emitEof(FileName, Call->getLine());
+ }
std::vector<Value*> Args;
if (!marshalCallArgs(Name, Call->getArgs(), Args)) return nullptr;
@@ -935,6 +959,67 @@ void CodeGen::emitStmt(StmtAST *Stmt) {
return;
}
+ if (auto *Open = dynamic_cast<OpenFileStmtAST*>(Stmt)) {
+ if (isWholeArrayVar(Open->getFileName())) {
+ reportError("Cannot use a whole array as the OPENFILE filename");
+ return;
+ }
+ Value *Name = coerceValueToType(emitExpr(Open->getFileName()), resolveType("STRING"));
+ if (!Name) return;
+ Files->emitOpen(Name, Open->getMode(), Open->getLine());
+ return;
+ }
+
+ if (auto *Read = dynamic_cast<ReadFileStmtAST*>(Stmt)) {
+ const SymbolInfo *Info = getSymbolInfo(Read->getVarName());
+ if (!Info) {
+ reportError("Unknown variable name %s", Read->getVarName().c_str());
+ return;
+ }
+ if (Info->IsArray) {
+ reportError("READFILE into entire array %s is not supported", Read->getVarName().c_str());
+ return;
+ }
+ if (Info->TypeName != "STRING") {
+ reportError("READFILE variable %s must be of type STRING (got %s)",
+ Read->getVarName().c_str(), Info->TypeName.c_str());
+ return;
+ }
+ if (isWholeArrayVar(Read->getFileName())) {
+ reportError("Cannot use a whole array as the READFILE filename");
+ return;
+ }
+ Value *Name = coerceValueToType(emitExpr(Read->getFileName()), resolveType("STRING"));
+ if (!Name) return;
+ Value *LineStr = Files->emitRead(Name, Read->getLine());
+ if (!LineStr) return;
+ Builder->CreateStore(LineStr, Info->Storage);
+ return;
+ }
+
+ if (auto *Write = dynamic_cast<WriteFileStmtAST*>(Stmt)) {
+ if (isWholeArrayVar(Write->getFileName()) || isWholeArrayVar(Write->getData())) {
+ reportError("Cannot use a whole array in WRITEFILE");
+ return;
+ }
+ Value *Name = coerceValueToType(emitExpr(Write->getFileName()), resolveType("STRING"));
+ Value *Data = coerceValueToType(emitExpr(Write->getData()), resolveType("STRING"));
+ if (!Name || !Data) return;
+ Files->emitWrite(Name, Data, Write->getLine());
+ return;
+ }
+
+ if (auto *Close = dynamic_cast<CloseFileStmtAST*>(Stmt)) {
+ if (isWholeArrayVar(Close->getFileName())) {
+ reportError("Cannot use a whole array as the CLOSEFILE filename");
+ return;
+ }
+ Value *Name = coerceValueToType(emitExpr(Close->getFileName()), resolveType("STRING"));
+ if (!Name) return;
+ Files->emitClose(Name, Close->getLine());
+ return;
+ }
+
if (auto *IfStmt = dynamic_cast<IfStmtAST*>(Stmt)) {
emitIfStmt(IfStmt);
return;
diff --git a/lib/CodeGen/FileHandler.cc b/lib/CodeGen/FileHandler.cc
new file mode 100644
index 0000000..275139a
--- /dev/null
+++ b/lib/CodeGen/FileHandler.cc
@@ -0,0 +1,452 @@
+#include "cps/FileHandler.h"
+#include "cps/RuntimeCheck.h"
+#include "llvm/IR/Constants.h"
+#include "llvm/IR/DerivedTypes.h"
+#include "llvm/IR/Function.h"
+#include "llvm/IR/Type.h"
+
+using namespace llvm;
+using namespace cps;
+
+FileHandler::FileHandler(LLVMContext &Ctx, IRBuilder<> &B, llvm::Module &M,
+ RuntimeCheck &RC)
+ : Context(Ctx), Builder(B), Module(M), Checker(RC) {
+ // Nothing is declared or emitted here: the whole runtime is synthesized
+ // lazily by ensureRuntime() so file-free programs keep their IR unchanged.
+}
+
+void FileHandler::setupExternalFunctions() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ Type *I64 = Type::getInt64Ty(Context);
+ Type *I32 = Type::getInt32Ty(Context);
+
+ FopenFunc = Module.getOrInsertFunction(
+ "fopen", FunctionType::get(PtrTy, {PtrTy, PtrTy}, false));
+ FcloseFunc = Module.getOrInsertFunction(
+ "fclose", FunctionType::get(I32, {PtrTy}, false));
+ FgetcFunc = Module.getOrInsertFunction(
+ "fgetc", FunctionType::get(I32, {PtrTy}, false));
+ UngetcFunc = Module.getOrInsertFunction(
+ "ungetc", FunctionType::get(I32, {I32, PtrTy}, false));
+ FputsFunc = Module.getOrInsertFunction(
+ "fputs", FunctionType::get(I32, {PtrTy, PtrTy}, false));
+ FputcFunc = Module.getOrInsertFunction(
+ "fputc", FunctionType::get(I32, {I32, PtrTy}, false));
+ MallocFunc = Module.getOrInsertFunction(
+ "malloc", FunctionType::get(PtrTy, {I64}, false));
+ ReallocFunc = Module.getOrInsertFunction(
+ "realloc", FunctionType::get(PtrTy, {PtrTy, I64}, false));
+ StrcmpFunc = Module.getOrInsertFunction(
+ "strcmp", FunctionType::get(I32, {PtrTy, PtrTy}, false));
+}
+
+void FileHandler::setupGlobals() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ ArrayType *PtrArrTy = ArrayType::get(PtrTy, TableSize);
+ ArrayType *ByteArrTy = ArrayType::get(Type::getInt8Ty(Context), TableSize);
+
+ FileNames = new GlobalVariable(Module, PtrArrTy, false, GlobalValue::InternalLinkage,
+ ConstantAggregateZero::get(PtrArrTy), "__cps_file_names");
+ FileHandles = new GlobalVariable(Module, PtrArrTy, false, GlobalValue::InternalLinkage,
+ ConstantAggregateZero::get(PtrArrTy), "__cps_file_handles");
+ FileModes = new GlobalVariable(Module, ByteArrTy, false, GlobalValue::InternalLinkage,
+ ConstantAggregateZero::get(ByteArrTy), "__cps_file_modes");
+
+ // Text mode on purpose (no "b"): on Windows the CRT translates CRLF<->LF,
+ // which is exactly what line-oriented READFILE/WRITEFILE want.
+ ModeReadStr = Builder.CreateGlobalStringPtr("r", "file_mode_r", 0, &Module);
+ ModeWriteStr = Builder.CreateGlobalStringPtr("w", "file_mode_w", 0, &Module);
+ ModeAppendStr = Builder.CreateGlobalStringPtr("a", "file_mode_a", 0, &Module);
+
+ ErrOpenFail = Builder.CreateGlobalStringPtr(
+ "[Fatal] line %d: Cannot open file '%s'\n", "err_file_open", 0, &Module);
+ ErrAlreadyOpen = Builder.CreateGlobalStringPtr(
+ "[Fatal] line %d: File '%s' is already open\n", "err_file_already_open", 0, &Module);
+ ErrTableFull = Builder.CreateGlobalStringPtr(
+ "[Fatal] line %d: Too many open files\n", "err_file_table_full", 0, &Module);
+ ErrNotOpen = Builder.CreateGlobalStringPtr(
+ "[Fatal] line %d: File '%s' is not open\n", "err_file_not_open", 0, &Module);
+ ErrNotRead = Builder.CreateGlobalStringPtr(
+ "[Fatal] line %d: File '%s' is not open for READ\n", "err_file_not_read", 0, &Module);
+ ErrNotWrite = Builder.CreateGlobalStringPtr(
+ "[Fatal] line %d: File '%s' is not open for WRITE\n", "err_file_not_write", 0, &Module);
+ ErrPastEof = Builder.CreateGlobalStringPtr(
+ "[Fatal] line %d: Read past end of file '%s'\n", "err_file_past_eof", 0, &Module);
+}
+
+void FileHandler::ensureRuntime() {
+ if (RuntimeEmitted) return;
+ RuntimeEmitted = true;
+
+ // Runs mid-statement-emission: the shared Builder sits at the end of an
+ // unterminated block in main() or a user function. Save/restore it around
+ // building the helper bodies (same discipline as FunctionDef emission).
+ BasicBlock *Saved = Builder.GetInsertBlock();
+
+ setupExternalFunctions();
+ setupGlobals();
+ buildFindFn();
+ buildOpenFn();
+ buildReadFn();
+ buildWriteFn();
+ buildCloseFn();
+ buildEofFn();
+
+ if (Saved) Builder.SetInsertPoint(Saved);
+}
+
+Function *FileHandler::createHelper(const char *Name, FunctionType *FT) {
+ Function *F = Function::Create(FT, Function::InternalLinkage, Name, &Module);
+ BasicBlock *Entry = BasicBlock::Create(Context, "entry", F);
+ Builder.SetInsertPoint(Entry);
+ return F;
+}
+
+Value *FileHandler::emitSlotGEP(GlobalVariable *Table, Type *ElemTy, Value *Idx) {
+ Value *Zero = ConstantInt::get(Context, APInt(64, 0));
+ return Builder.CreateInBoundsGEP(ArrayType::get(ElemTy, TableSize), Table, {Zero, Idx});
+}
+
+// Shared prologue of every helper that operates on an already-open file:
+// look the name up, die with "not open" on a miss, return the slot index.
+Value *FileHandler::emitLookupOrFail(Value *Name, Value *Line) {
+ Value *Idx = Builder.CreateCall(FindFn, {Name}, "slot");
+ Value *NotOpen = Builder.CreateICmpSLT(Idx, ConstantInt::get(Context, APInt(64, 0)), "not_open");
+ Checker.emitErrorAndExit(NotOpen, ErrNotOpen, {Line, Name});
+ return Idx;
+}
+
+// i64 __cps_file_find(ptr name): strcmp scan over occupied slots; -1 on miss.
+void FileHandler::buildFindFn() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ Type *I64 = Type::getInt64Ty(Context);
+ Type *I8 = Type::getInt8Ty(Context);
+
+ FindFn = createHelper("__cps_file_find", FunctionType::get(I64, {PtrTy}, false));
+ Value *Name = FindFn->getArg(0);
+ Name->setName("name");
+
+ AllocaInst *IdxVar = Builder.CreateAlloca(I64, nullptr, "idx");
+ Builder.CreateStore(ConstantInt::get(Context, APInt(64, 0)), IdxVar);
+
+ BasicBlock *LoopBB = BasicBlock::Create(Context, "loop", FindFn);
+ BasicBlock *CheckBB = BasicBlock::Create(Context, "check_slot", FindFn);
+ BasicBlock *CmpBB = BasicBlock::Create(Context, "cmp_name", FindFn);
+ BasicBlock *NextBB = BasicBlock::Create(Context, "next", FindFn);
+ BasicBlock *FoundBB = BasicBlock::Create(Context, "found", FindFn);
+ BasicBlock *MissBB = BasicBlock::Create(Context, "miss", FindFn);
+
+ Builder.CreateBr(LoopBB);
+
+ Builder.SetInsertPoint(LoopBB);
+ Value *Idx = Builder.CreateLoad(I64, IdxVar, "i");
+ Value *AtEnd = Builder.CreateICmpSGE(Idx, ConstantInt::get(Context, APInt(64, TableSize)), "at_end");
+ Builder.CreateCondBr(AtEnd, MissBB, CheckBB);
+
+ Builder.SetInsertPoint(CheckBB);
+ Value *Mode = Builder.CreateLoad(I8, emitSlotGEP(FileModes, I8, Idx), "mode");
+ Value *IsFree = Builder.CreateICmpEQ(Mode, ConstantInt::get(I8, 0), "is_free");
+ Builder.CreateCondBr(IsFree, NextBB, CmpBB);
+
+ Builder.SetInsertPoint(CmpBB);
+ Value *SlotName = Builder.CreateLoad(PtrTy, emitSlotGEP(FileNames, PtrTy, Idx), "slot_name");
+ Value *Cmp = Builder.CreateCall(StrcmpFunc, {SlotName, Name}, "cmp");
+ Value *IsEq = Builder.CreateICmpEQ(Cmp, ConstantInt::get(Context, APInt(32, 0)), "is_eq");
+ Builder.CreateCondBr(IsEq, FoundBB, NextBB);
+
+ Builder.SetInsertPoint(NextBB);
+ Value *NextIdx = Builder.CreateAdd(Idx, ConstantInt::get(Context, APInt(64, 1)));
+ Builder.CreateStore(NextIdx, IdxVar);
+ Builder.CreateBr(LoopBB);
+
+ Builder.SetInsertPoint(FoundBB);
+ Builder.CreateRet(Idx);
+
+ Builder.SetInsertPoint(MissBB);
+ Builder.CreateRet(ConstantInt::get(Context, APInt(64, -1, /*isSigned=*/true)));
+}
+
+// void __cps_file_open(ptr name, i8 mode, i32 line)
+void FileHandler::buildOpenFn() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ Type *I64 = Type::getInt64Ty(Context);
+ Type *I32 = Type::getInt32Ty(Context);
+ Type *I8 = Type::getInt8Ty(Context);
+ Type *VoidTy = Type::getVoidTy(Context);
+
+ OpenFn = createHelper("__cps_file_open", FunctionType::get(VoidTy, {PtrTy, I8, I32}, false));
+ Value *Name = OpenFn->getArg(0);
+ Value *Mode = OpenFn->getArg(1);
+ Value *Line = OpenFn->getArg(2);
+ Name->setName("name");
+ Mode->setName("mode");
+ Line->setName("line");
+
+ Value *Existing = Builder.CreateCall(FindFn, {Name}, "existing");
+ Value *AlreadyOpen = Builder.CreateICmpSGE(Existing, ConstantInt::get(Context, APInt(64, 0)), "already_open");
+ Checker.emitErrorAndExit(AlreadyOpen, ErrAlreadyOpen, {Line, Name});
+
+ // First-free-slot scan; SlotVar stays -1 when the table is full.
+ AllocaInst *IdxVar = Builder.CreateAlloca(I64, nullptr, "idx");
+ AllocaInst *SlotVar = Builder.CreateAlloca(I64, nullptr, "slot");
+ Builder.CreateStore(ConstantInt::get(Context, APInt(64, 0)), IdxVar);
+ Builder.CreateStore(ConstantInt::get(Context, APInt(64, -1, /*isSigned=*/true)), SlotVar);
+
+ BasicBlock *ScanBB = BasicBlock::Create(Context, "scan", OpenFn);
+ BasicBlock *CheckBB = BasicBlock::Create(Context, "check_free", OpenFn);
+ BasicBlock *TakeBB = BasicBlock::Create(Context, "take_slot", OpenFn);
+ BasicBlock *NextBB = BasicBlock::Create(Context, "next", OpenFn);
+ BasicBlock *AfterBB = BasicBlock::Create(Context, "after_scan", OpenFn);
+
+ Builder.CreateBr(ScanBB);
+
+ Builder.SetInsertPoint(ScanBB);
+ Value *Idx = Builder.CreateLoad(I64, IdxVar, "i");
+ Value *AtEnd = Builder.CreateICmpSGE(Idx, ConstantInt::get(Context, APInt(64, TableSize)), "at_end");
+ Builder.CreateCondBr(AtEnd, AfterBB, CheckBB);
+
+ Builder.SetInsertPoint(CheckBB);
+ Value *SlotMode = Builder.CreateLoad(I8, emitSlotGEP(FileModes, I8, Idx), "slot_mode");
+ Value *IsFree = Builder.CreateICmpEQ(SlotMode, ConstantInt::get(I8, 0), "is_free");
+ Builder.CreateCondBr(IsFree, TakeBB, NextBB);
+
+ Builder.SetInsertPoint(TakeBB);
+ Builder.CreateStore(Idx, SlotVar);
+ Builder.CreateBr(AfterBB);
+
+ Builder.SetInsertPoint(NextBB);
+ Builder.CreateStore(Builder.CreateAdd(Idx, ConstantInt::get(Context, APInt(64, 1))), IdxVar);
+ Builder.CreateBr(ScanBB);
+
+ Builder.SetInsertPoint(AfterBB);
+ Value *Slot = Builder.CreateLoad(I64, SlotVar, "free_slot");
+ Value *NoFree = Builder.CreateICmpSLT(Slot, ConstantInt::get(Context, APInt(64, 0)), "table_full");
+ Checker.emitErrorAndExit(NoFree, ErrTableFull, {Line});
+
+ // Branch-free fopen-mode selection: 1 -> "r", 2 -> "w", 3 -> "a".
+ Value *IsWrite = Builder.CreateICmpEQ(Mode, ConstantInt::get(I8, uint8_t(FileMode::Write)));
+ Value *WriteOrAppend = Builder.CreateSelect(IsWrite, ModeWriteStr, ModeAppendStr);
+ Value *IsRead = Builder.CreateICmpEQ(Mode, ConstantInt::get(I8, uint8_t(FileMode::Read)));
+ Value *ModeStr = Builder.CreateSelect(IsRead, ModeReadStr, WriteOrAppend, "mode_str");
+
+ Value *Handle = Builder.CreateCall(FopenFunc, {Name, ModeStr}, "handle");
+ Value *OpenFailed = Builder.CreateICmpEQ(Handle, ConstantPointerNull::get(PtrTy), "open_failed");
+ Checker.emitErrorAndExit(OpenFailed, ErrOpenFail, {Line, Name});
+
+ Builder.CreateStore(Name, emitSlotGEP(FileNames, PtrTy, Slot));
+ Builder.CreateStore(Handle, emitSlotGEP(FileHandles, PtrTy, Slot));
+ Builder.CreateStore(Mode, emitSlotGEP(FileModes, I8, Slot));
+ Builder.CreateRetVoid();
+}
+
+// ptr __cps_file_read(ptr name, i32 line): read one line into a fresh heap
+// STRING ('\n' consumed and stripped, trailing '\r' dropped). Reading at EOF
+// is a fatal error — CIE programs are expected to guard with EOF().
+void FileHandler::buildReadFn() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ Type *I64 = Type::getInt64Ty(Context);
+ Type *I32 = Type::getInt32Ty(Context);
+ Type *I8 = Type::getInt8Ty(Context);
+
+ ReadFn = createHelper("__cps_file_read", FunctionType::get(PtrTy, {PtrTy, I32}, false));
+ Value *Name = ReadFn->getArg(0);
+ Value *Line = ReadFn->getArg(1);
+ Name->setName("name");
+ Line->setName("line");
+
+ Value *Idx = emitLookupOrFail(Name, Line);
+ Value *Mode = Builder.CreateLoad(I8, emitSlotGEP(FileModes, I8, Idx), "mode");
+ Value *NotRead = Builder.CreateICmpNE(Mode, ConstantInt::get(I8, uint8_t(FileMode::Read)), "not_read");
+ Checker.emitErrorAndExit(NotRead, ErrNotRead, {Line, Name});
+ Value *Handle = Builder.CreateLoad(PtrTy, emitSlotGEP(FileHandles, PtrTy, Idx), "handle");
+
+ Value *MinusOne = ConstantInt::get(Context, APInt(32, -1, /*isSigned=*/true));
+ Value *Peek = Builder.CreateCall(FgetcFunc, {Handle}, "peek");
+ Value *AtEof = Builder.CreateICmpEQ(Peek, MinusOne, "at_eof");
+ Checker.emitErrorAndExit(AtEof, ErrPastEof, {Line, Name});
+ Builder.CreateCall(UngetcFunc, {Peek, Handle});
+
+ // Growing buffer: cap doubles, always leaving room for byte + NUL.
+ AllocaInst *BufVar = Builder.CreateAlloca(PtrTy, nullptr, "buf");
+ AllocaInst *LenVar = Builder.CreateAlloca(I64, nullptr, "len");
+ AllocaInst *CapVar = Builder.CreateAlloca(I64, nullptr, "cap");
+ AllocaInst *ChVar = Builder.CreateAlloca(I32, nullptr, "ch");
+ Value *InitCap = ConstantInt::get(Context, APInt(64, 64));
+ Builder.CreateStore(Builder.CreateCall(MallocFunc, {InitCap}, "line_buf"), BufVar);
+ Builder.CreateStore(ConstantInt::get(Context, APInt(64, 0)), LenVar);
+ Builder.CreateStore(InitCap, CapVar);
+
+ BasicBlock *LoopBB = BasicBlock::Create(Context, "read_loop", ReadFn);
+ BasicBlock *EnsureBB = BasicBlock::Create(Context, "ensure_cap", ReadFn);
+ BasicBlock *GrowBB = BasicBlock::Create(Context, "grow", ReadFn);
+ BasicBlock *StoreBB = BasicBlock::Create(Context, "store_char", ReadFn);
+ BasicBlock *StripBB = BasicBlock::Create(Context, "strip", ReadFn);
+ BasicBlock *CheckCrBB = BasicBlock::Create(Context, "check_cr", ReadFn);
+ BasicBlock *DropCrBB = BasicBlock::Create(Context, "drop_cr", ReadFn);
+ BasicBlock *DoneBB = BasicBlock::Create(Context, "done", ReadFn);
+
+ Builder.CreateBr(LoopBB);
+
+ Builder.SetInsertPoint(LoopBB);
+ Value *Ch = Builder.CreateCall(FgetcFunc, {Handle}, "c");
+ Builder.CreateStore(Ch, ChVar);
+ Value *IsEof = Builder.CreateICmpEQ(Ch, MinusOne);
+ Value *IsNl = Builder.CreateICmpEQ(Ch, ConstantInt::get(Context, APInt(32, '\n')));
+ Value *Stop = Builder.CreateOr(IsEof, IsNl, "stop");
+ Builder.CreateCondBr(Stop, StripBB, EnsureBB);
+
+ Builder.SetInsertPoint(EnsureBB);
+ Value *Len = Builder.CreateLoad(I64, LenVar, "len");
+ Value *Cap = Builder.CreateLoad(I64, CapVar, "cap");
+ Value *Need = Builder.CreateAdd(Len, ConstantInt::get(Context, APInt(64, 2)), "need");
+ Value *TooSmall = Builder.CreateICmpSGT(Need, Cap, "too_small");
+ Builder.CreateCondBr(TooSmall, GrowBB, StoreBB);
+
+ Builder.SetInsertPoint(GrowBB);
+ Value *NewCap = Builder.CreateShl(Cap, ConstantInt::get(Context, APInt(64, 1)), "new_cap");
+ Builder.CreateStore(NewCap, CapVar);
+ Value *OldBuf = Builder.CreateLoad(PtrTy, BufVar);
+ Builder.CreateStore(Builder.CreateCall(ReallocFunc, {OldBuf, NewCap}, "regrown"), BufVar);
+ Builder.CreateBr(StoreBB);
+
+ Builder.SetInsertPoint(StoreBB);
+ Value *Buf = Builder.CreateLoad(PtrTy, BufVar);
+ Value *CurLen = Builder.CreateLoad(I64, LenVar);
+ Value *DestPtr = Builder.CreateInBoundsGEP(I8, Buf, CurLen);
+ Value *Byte = Builder.CreateTrunc(Builder.CreateLoad(I32, ChVar), I8);
+ Builder.CreateStore(Byte, DestPtr);
+ Builder.CreateStore(Builder.CreateAdd(CurLen, ConstantInt::get(Context, APInt(64, 1))), LenVar);
+ Builder.CreateBr(LoopBB);
+
+ // Normalize CRLF: drop one trailing '\r' (covers reading Windows-authored
+ // files on Linux; Windows text mode has already translated).
+ Builder.SetInsertPoint(StripBB);
+ Value *EndLen = Builder.CreateLoad(I64, LenVar, "end_len");
+ Value *HasChars = Builder.CreateICmpSGT(EndLen, ConstantInt::get(Context, APInt(64, 0)));
+ Builder.CreateCondBr(HasChars, CheckCrBB, DoneBB);
+
+ Builder.SetInsertPoint(CheckCrBB);
+ Value *EndBuf = Builder.CreateLoad(PtrTy, BufVar);
+ Value *LastIdx = Builder.CreateSub(EndLen, ConstantInt::get(Context, APInt(64, 1)));
+ Value *LastByte = Builder.CreateLoad(I8, Builder.CreateInBoundsGEP(I8, EndBuf, LastIdx), "last");
+ Value *IsCr = Builder.CreateICmpEQ(LastByte, ConstantInt::get(I8, '\r'), "is_cr");
+ Builder.CreateCondBr(IsCr, DropCrBB, DoneBB);
+
+ Builder.SetInsertPoint(DropCrBB);
+ Builder.CreateStore(LastIdx, LenVar);
+ Builder.CreateBr(DoneBB);
+
+ Builder.SetInsertPoint(DoneBB);
+ Value *FinalBuf = Builder.CreateLoad(PtrTy, BufVar);
+ Value *FinalLen = Builder.CreateLoad(I64, LenVar);
+ Builder.CreateStore(ConstantInt::get(I8, 0), Builder.CreateInBoundsGEP(I8, FinalBuf, FinalLen));
+ Builder.CreateRet(FinalBuf);
+}
+
+// void __cps_file_write(ptr name, ptr data, i32 line): one line, WRITE/APPEND only.
+void FileHandler::buildWriteFn() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ Type *I32 = Type::getInt32Ty(Context);
+ Type *I8 = Type::getInt8Ty(Context);
+
+ WriteFn = createHelper("__cps_file_write",
+ FunctionType::get(Type::getVoidTy(Context), {PtrTy, PtrTy, I32}, false));
+ Value *Name = WriteFn->getArg(0);
+ Value *Data = WriteFn->getArg(1);
+ Value *Line = WriteFn->getArg(2);
+ Name->setName("name");
+ Data->setName("data");
+ Line->setName("line");
+
+ Value *Idx = emitLookupOrFail(Name, Line);
+ Value *Mode = Builder.CreateLoad(I8, emitSlotGEP(FileModes, I8, Idx), "mode");
+ Value *IsReadMode = Builder.CreateICmpEQ(Mode, ConstantInt::get(I8, uint8_t(FileMode::Read)), "read_only");
+ Checker.emitErrorAndExit(IsReadMode, ErrNotWrite, {Line, Name});
+
+ Value *Handle = Builder.CreateLoad(PtrTy, emitSlotGEP(FileHandles, PtrTy, Idx), "handle");
+ Builder.CreateCall(FputsFunc, {Data, Handle});
+ Builder.CreateCall(FputcFunc, {ConstantInt::get(Context, APInt(32, '\n')), Handle});
+ Builder.CreateRetVoid();
+}
+
+// void __cps_file_close(ptr name, i32 line)
+void FileHandler::buildCloseFn() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ Type *I32 = Type::getInt32Ty(Context);
+ Type *I8 = Type::getInt8Ty(Context);
+
+ CloseFn = createHelper("__cps_file_close",
+ FunctionType::get(Type::getVoidTy(Context), {PtrTy, I32}, false));
+ Value *Name = CloseFn->getArg(0);
+ Value *Line = CloseFn->getArg(1);
+ Name->setName("name");
+ Line->setName("line");
+
+ Value *Idx = emitLookupOrFail(Name, Line);
+ Value *Handle = Builder.CreateLoad(PtrTy, emitSlotGEP(FileHandles, PtrTy, Idx), "handle");
+ Builder.CreateCall(FcloseFunc, {Handle});
+ Builder.CreateStore(ConstantInt::get(I8, 0), emitSlotGEP(FileModes, I8, Idx));
+ Builder.CreateStore(ConstantPointerNull::get(PtrTy), emitSlotGEP(FileNames, PtrTy, Idx));
+ Builder.CreateStore(ConstantPointerNull::get(PtrTy), emitSlotGEP(FileHandles, PtrTy, Idx));
+ Builder.CreateRetVoid();
+}
+
+// i1 __cps_file_eof(ptr name, i32 line): TRUE iff no more lines can be read.
+void FileHandler::buildEofFn() {
+ PointerType *PtrTy = PointerType::getUnqual(Context);
+ Type *I32 = Type::getInt32Ty(Context);
+ Type *I8 = Type::getInt8Ty(Context);
+
+ EofFn = createHelper("__cps_file_eof",
+ FunctionType::get(Type::getInt1Ty(Context), {PtrTy, I32}, false));
+ Value *Name = EofFn->getArg(0);
+ Value *Line = EofFn->getArg(1);
+ Name->setName("name");
+ Line->setName("line");
+
+ Value *Idx = emitLookupOrFail(Name, Line);
+ Value *Mode = Builder.CreateLoad(I8, emitSlotGEP(FileModes, I8, Idx), "mode");
+ Value *NotRead = Builder.CreateICmpNE(Mode, ConstantInt::get(I8, uint8_t(FileMode::Read)), "not_read");
+ Checker.emitErrorAndExit(NotRead, ErrNotRead, {Line, Name});
+
+ Value *Handle = Builder.CreateLoad(PtrTy, emitSlotGEP(FileHandles, PtrTy, Idx), "handle");
+ Value *Ch = Builder.CreateCall(FgetcFunc, {Handle}, "c");
+ // ungetc(EOF, f) is a defined no-op, so no branch is needed.
+ Builder.CreateCall(UngetcFunc, {Ch, Handle});
+ Value *IsEof = Builder.CreateICmpEQ(Ch, ConstantInt::get(Context, APInt(32, -1, /*isSigned=*/true)), "is_eof");
+ Builder.CreateRet(IsEof);
+}
+
+void FileHandler::emitOpen(Value *NameStr, FileMode Mode, int Line) {
+ if (!NameStr) return;
+ ensureRuntime();
+ Builder.CreateCall(OpenFn, {NameStr,
+ ConstantInt::get(Type::getInt8Ty(Context), uint8_t(Mode)),
+ ConstantInt::get(Context, APInt(32, Line))});
+}
+
+Value *FileHandler::emitRead(Value *NameStr, int Line) {
+ if (!NameStr) return nullptr;
+ ensureRuntime();
+ return Builder.CreateCall(ReadFn, {NameStr, ConstantInt::get(Context, APInt(32, Line))},
+ "file_line");
+}
+
+void FileHandler::emitWrite(Value *NameStr, Value *DataStr, int Line) {
+ if (!NameStr || !DataStr) return;
+ ensureRuntime();
+ Builder.CreateCall(WriteFn, {NameStr, DataStr, ConstantInt::get(Context, APInt(32, Line))});
+}
+
+void FileHandler::emitClose(Value *NameStr, int Line) {
+ if (!NameStr) return;
+ ensureRuntime();
+ Builder.CreateCall(CloseFn, {NameStr, ConstantInt::get(Context, APInt(32, Line))});
+}
+
+Value *FileHandler::emitEof(Value *NameStr, int Line) {
+ if (!NameStr) return nullptr;
+ ensureRuntime();
+ return Builder.CreateCall(EofFn, {NameStr, ConstantInt::get(Context, APInt(32, Line))},
+ "file_eof");
+}
diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc
index b778bbe..df0a8b6 100644
--- a/lib/CodeGen/FunctionGen.cc
+++ b/lib/CodeGen/FunctionGen.cc
@@ -12,6 +12,8 @@ static bool isReservedRuntimeName(const std::string &Name) {
"main", "printf", "scanf", "malloc", "free", "strlen", "memcpy",
"strcpy", "strcat", "toupper", "tolower", "sprintf", "strtol",
"strtod", "memset", "exit",
+ "fopen", "fclose", "fgetc", "ungetc", "fputs", "fputc",
+ "realloc", "strcmp",
};
for (const char *R : Reserved) {
if (Name == R) return true;
diff --git a/lib/CodeGen/RuntimeCheck.cc b/lib/CodeGen/RuntimeCheck.cc
index d51d78a..c2ced87 100644
--- a/lib/CodeGen/RuntimeCheck.cc
+++ b/lib/CodeGen/RuntimeCheck.cc
@@ -27,8 +27,12 @@ void RuntimeCheck::setupExternalFunctions() {
}
void RuntimeCheck::emitErrorAndExit(Value *Condition, Value *Msg, int Line) {
+ emitErrorAndExit(Condition, Msg, {ConstantInt::get(TheContext, APInt(32, Line))});
+}
+
+void RuntimeCheck::emitErrorAndExit(Value *Condition, Value *Msg, ArrayRef<Value *> FmtArgs) {
Function *TheFunction = Builder.GetInsertBlock()->getParent();
-
+
BasicBlock *FailBB = BasicBlock::Create(TheContext, "check_fail", TheFunction);
BasicBlock *ContBB = BasicBlock::Create(TheContext, "check_cont", TheFunction);
@@ -37,7 +41,7 @@ void RuntimeCheck::emitErrorAndExit(Value *Condition, Value *Msg, int Line) {
Builder.SetInsertPoint(FailBB);
std::vector<Value*> PrintArgs;
PrintArgs.push_back(Msg);
- PrintArgs.push_back(ConstantInt::get(TheContext, APInt(32, Line)));
+ PrintArgs.insert(PrintArgs.end(), FmtArgs.begin(), FmtArgs.end());
Builder.CreateCall(PrintfFunc, PrintArgs);
Builder.CreateCall(ExitFunc, ConstantInt::get(TheContext, APInt(32, 1)));
diff --git a/lib/Lexer/Lexer.cc b/lib/Lexer/Lexer.cc
index a8af837..3c56271 100644
--- a/lib/Lexer/Lexer.cc
+++ b/lib/Lexer/Lexer.cc
@@ -121,6 +121,15 @@ int Lexer::gettok() {
if (IdentifierStr == "BYREF") return tok_byref;
if (IdentifierStr == "BYVAL") return tok_byval;
+ if (IdentifierStr == "OPENFILE") return tok_openfile;
+ if (IdentifierStr == "READFILE") return tok_readfile;
+ if (IdentifierStr == "WRITEFILE") return tok_writefile;
+ if (IdentifierStr == "CLOSEFILE") return tok_closefile;
+ if (IdentifierStr == "READ") return tok_read;
+ if (IdentifierStr == "WRITE") return tok_write;
+ if (IdentifierStr == "APPEND") return tok_append;
+ if (IdentifierStr == "RANDOM") return tok_random;
+
return tok_identifier;
}
diff --git a/lib/Parser/Parser.cc b/lib/Parser/Parser.cc
index 77df9bb..e67dda7 100644
--- a/lib/Parser/Parser.cc
+++ b/lib/Parser/Parser.cc
@@ -59,7 +59,7 @@ std::unique_ptr<ExprAST> Parser::ParseIdentifierExpr() {
getNextToken();
auto Args = ParseExprList(')', true);
if (!Args) return nullptr;
- return std::make_unique<CallExprAST>(IdName, std::move(*Args));
+ return std::make_unique<CallExprAST>(IdName, std::move(*Args), Line);
}
if (CurTok == '[') {
@@ -213,6 +213,7 @@ void Parser::syncToStatementStart() {
case tok_function: case tok_procedure: case tok_call: case tok_return:
case tok_else: case tok_endif: case tok_endwhile: case tok_until:
case tok_next: case tok_endfunction: case tok_endprocedure:
+ case tok_openfile: case tok_readfile: case tok_writefile: case tok_closefile:
return;
default:
getNextToken();
@@ -537,6 +538,18 @@ std::unique_ptr<StmtAST> Parser::ParseStatementImpl() {
else if (CurTok == tok_return) {
return ParseReturnStmt();
}
+ else if (CurTok == tok_openfile) {
+ return ParseOpenFile();
+ }
+ else if (CurTok == tok_readfile) {
+ return ParseReadFile();
+ }
+ else if (CurTok == tok_writefile) {
+ return ParseWriteFile();
+ }
+ else if (CurTok == tok_closefile) {
+ return ParseCloseFile();
+ }
fprintf(stderr, "Error: Unexpected token at line %d when expecting a statement\n", Lex.getLine());
getNextToken(); // guarantee progress so error recovery cannot loop forever
diff --git a/lib/Parser/ParserFile.cc b/lib/Parser/ParserFile.cc
new file mode 100644
index 0000000..6fd368c
--- /dev/null
+++ b/lib/Parser/ParserFile.cc
@@ -0,0 +1,92 @@
+#include "cps/Parser.h"
+#include "cps/FileAST.h"
+#include <cstdio>
+
+using namespace cps;
+
+// OPENFILE <expr> FOR READ|WRITE|APPEND
+std::unique_ptr<StmtAST> Parser::ParseOpenFile() {
+ int Line = Lex.getLine();
+ getNextToken(); // eat OPENFILE
+
+ auto FileName = ParseExpression();
+ if (!FileName) return nullptr;
+
+ if (CurTok != tok_for) {
+ fprintf(stderr, "Error: Expected FOR after OPENFILE filename at line %d\n", Lex.getLine());
+ return nullptr;
+ }
+ getNextToken(); // eat FOR
+
+ FileMode Mode;
+ switch (CurTok) {
+ case tok_read: Mode = FileMode::Read; break;
+ case tok_write: Mode = FileMode::Write; break;
+ case tok_append: Mode = FileMode::Append; break;
+ case tok_random:
+ fprintf(stderr, "Error: File mode RANDOM is not implemented (text files only) at line %d\n",
+ Lex.getLine());
+ return nullptr;
+ default:
+ fprintf(stderr, "Error: Expected file mode READ, WRITE or APPEND at line %d\n", Lex.getLine());
+ return nullptr;
+ }
+ getNextToken(); // eat the mode keyword
+
+ return std::make_unique<OpenFileStmtAST>(std::move(FileName), Mode, Line);
+}
+
+// READFILE <expr>, <identifier>
+std::unique_ptr<StmtAST> Parser::ParseReadFile() {
+ int Line = Lex.getLine();
+ getNextToken(); // eat READFILE
+
+ auto FileName = ParseExpression();
+ if (!FileName) return nullptr;
+
+ if (CurTok != ',') {
+ fprintf(stderr, "Error: Expected ',' after READFILE filename at line %d\n", Lex.getLine());
+ return nullptr;
+ }
+ getNextToken(); // eat ','
+
+ if (CurTok != tok_identifier) {
+ fprintf(stderr, "Error: Expected variable name after ',' in READFILE at line %d\n", Lex.getLine());
+ return nullptr;
+ }
+ std::string VarName = Lex.IdentifierStr;
+ getNextToken(); // eat the variable name
+
+ return std::make_unique<ReadFileStmtAST>(std::move(FileName), VarName, Line);
+}
+
+// WRITEFILE <expr>, <expr>
+std::unique_ptr<StmtAST> Parser::ParseWriteFile() {
+ int Line = Lex.getLine();
+ getNextToken(); // eat WRITEFILE
+
+ auto FileName = ParseExpression();
+ if (!FileName) return nullptr;
+
+ if (CurTok != ',') {
+ fprintf(stderr, "Error: Expected ',' after WRITEFILE filename at line %d\n", Lex.getLine());
+ return nullptr;
+ }
+ getNextToken(); // eat ','
+
+ auto Data = ParseExpression();
+ if (!Data) return nullptr;
+
+ return std::make_unique<WriteFileStmtAST>(std::move(FileName), std::move(Data), Line);
+}
+
+// CLOSEFILE <expr>
+std::unique_ptr<StmtAST> Parser::ParseCloseFile() {
+ int Line = Lex.getLine();
+ getNextToken(); // eat CLOSEFILE
+
+ auto FileName = ParseExpression();
+ if (!FileName) return nullptr;
+
+ return std::make_unique<CloseFileStmtAST>(std::move(FileName), Line);
+}