diff options
| -rw-r--r-- | AGENTS/ARCH.md | 5 | ||||
| -rw-r--r-- | AGENTS/CONVENTIONS.md | 26 | ||||
| -rw-r--r-- | AGENTS/EXTENDING.md | 3 | ||||
| -rw-r--r-- | AGENTS/PITFALLS.md | 23 | ||||
| -rw-r--r-- | AGENTS/SUBSYSTEMS.md | 27 | ||||
| -rw-r--r-- | CMakeLists.txt | 4 | ||||
| -rw-r--r-- | README.md | 5 | ||||
| -rw-r--r-- | include/cps/CodeGen.h | 3 | ||||
| -rw-r--r-- | include/cps/FileAST.h | 63 | ||||
| -rw-r--r-- | include/cps/FileHandler.h | 75 | ||||
| -rw-r--r-- | include/cps/FileMode.h | 11 | ||||
| -rw-r--r-- | include/cps/FunctionAST.h | 6 | ||||
| -rw-r--r-- | include/cps/Lexer.h | 11 | ||||
| -rw-r--r-- | include/cps/Parser.h | 5 | ||||
| -rw-r--r-- | include/cps/RuntimeCheck.h | 7 | ||||
| -rw-r--r-- | lib/CodeGen/ArithmeticHandler.cc | 60 | ||||
| -rw-r--r-- | lib/CodeGen/CodeGen.cc | 85 | ||||
| -rw-r--r-- | lib/CodeGen/FileHandler.cc | 452 | ||||
| -rw-r--r-- | lib/CodeGen/FunctionGen.cc | 2 | ||||
| -rw-r--r-- | lib/CodeGen/RuntimeCheck.cc | 8 | ||||
| -rw-r--r-- | lib/Lexer/Lexer.cc | 9 | ||||
| -rw-r--r-- | lib/Parser/Parser.cc | 15 | ||||
| -rw-r--r-- | lib/Parser/ParserFile.cc | 92 |
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) @@ -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); +} |
