diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-13 03:23:52 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-13 03:23:52 +0000 |
| commit | f9a0c7e9687bc3e113f4c8f44861533c9ca48b3a (patch) | |
| tree | 5d51c5a403dd096b0c3748118409be20eb3e1900 | |
| parent | 3818460a473bd3ffcd35f09eb6429e1b0f00bc07 (diff) | |
feat: compile source files with configurable IR output
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
| -rw-r--r-- | AGENTS/ARCH.md | 25 | ||||
| -rw-r--r-- | AGENTS/CONVENTIONS.md | 6 | ||||
| -rw-r--r-- | AGENTS/PITFALLS.md | 4 | ||||
| -rw-r--r-- | README.md | 13 | ||||
| -rw-r--r-- | include/cps/CodeGen.h | 2 | ||||
| -rw-r--r-- | include/cps/Lexer.h | 5 | ||||
| -rw-r--r-- | lib/CodeGen/CodeGen.cc | 4 | ||||
| -rw-r--r-- | lib/Lexer/Lexer.cc | 64 | ||||
| -rw-r--r-- | tools/driver/main.cc | 84 |
9 files changed, 150 insertions, 57 deletions
diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md index 4ed2404..0161286 100644 --- a/AGENTS/ARCH.md +++ b/AGENTS/ARCH.md @@ -4,8 +4,8 @@ Kaleidoscope-style design, C++17, no external deps beyond LLVM 18 and libc. ``` -stdin ──> Lexer ──> Parser ──> vector<unique_ptr<StmtAST>> ──> CodeGen ──> textual LLVM IR on stderr - (getchar) (recursive (flat program list; (one llvm::Module "cps_module", +file ──> Lexer ──> Parser ──> vector<unique_ptr<StmtAST>> ──> CodeGen ──> textual LLVM IR in a file + (istream) (recursive (flat program list; (one llvm::Module "cps_module", descent) FunctionDefAST mixed in) everything else inside i32 main()) ``` @@ -24,20 +24,21 @@ The compiler stops at textual IR. Making a runnable binary is manual: ```bash mkdir -p build && cd build && cmake -DLLVM_DIR=/usr/lib/llvm-18/cmake .. && make # build cpsc -./cpsc < prog.cps 2> prog.ll # stdin is the ONLY input; IR goes to STDERR +./cpsc prog.cps -o prog.ll # default without -o: prog.ir beside the source clang-18 prog.ll -o prog && ./prog ``` -Facts about the driver (`tools/driver/main.cc`) that break naive automation: +Driver behavior (`tools/driver/main.cc`): -- **No argv.** `./cpsc file.cps` blocks forever on stdin; you must redirect `<`. -- **IR and error messages share stderr.** A failed compile still dumps (partial) IR, - mixed with `Error:` lines that make clang reject the file. -- **Exit code is non-zero iff a diagnostic was emitted.** Lexer, Parser and CodeGen each - track an error flag (`hadError()`); every failure path prints an `Error:` line, and - `main` returns 1 when any flag is set. The emitted module is also checked with - `verifyModule`, so invalid IR flips the exit code too. Check the exit code — do not - grep stderr. +- One source file, with optional `-o path`; options may precede or follow the source. + No stdin source input. No arguments exits silently with status 0 (help TODO). +- IR is written only after successful parsing, code generation and module + verification. Diagnostics stay on stderr; failures do not overwrite existing IR. +- Lexer, Parser and CodeGen track error flags (`hadError()`); any error, including + invalid IR from `verifyModule`, makes the driver return 1. File/argument errors + also return 1. Check the exit code rather than grepping stderr. +- Successful compilation overwrites the output, but paths aliasing the source + (including symlinks/hard links) are rejected. Build pin: developed and tested against **LLVM 18**. (The historical LLVM 19 breakage — `ArithmeticHandler.cc` touching `llvm::Module` through a transitive include — is gone diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md index 7c6956d..6681d1a 100644 --- a/AGENTS/CONVENTIONS.md +++ b/AGENTS/CONVENTIONS.md @@ -15,9 +15,9 @@ identifier. Assignment is `<-` (ASCII only), equality is single `=`, inequality concatenation `&` (reaches the parser as raw ASCII 38 — no token). No escape sequences in string/char literals. Comments are `//` to end of line. -**The lexer can run once per process, from stdin only.** `gettok()` keeps its lookahead -in a function-local `static int LastChar` and calls `getchar()` directly. No re-lexing, -no file paths, and unit tests can't lex two programs in one process. +**The lexer reads a caller-owned stream.** Construct it with `Lexer(std::istream &)`; +the stream must outlive the lexer. Lookahead, line number and error state belong to +each instance. The driver opens the source file; stdin is not used for compilation. **Types travel as uppercase `std::string`** (`"INTEGER"`, `"REAL"`, ..., plus user-defined names) — never an enum. `TypeSystem` resolves the string to diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md index 7fddfff..acf21d2 100644 --- a/AGENTS/PITFALLS.md +++ b/AGENTS/PITFALLS.md @@ -38,8 +38,8 @@ test programs. unknown-function calls; OUTPUT reports an error for a nullptr result. - The lexer emits no newline tokens; the grammar is purely keyword-delimited. Bare `RETURN` is only recognized right before ENDIF/ELSE/ENDFUNCTION/ENDPROCEDURE. -- Invalid modules still print their IR, but `verifyModule` runs at the end of - `compile()` and makes the driver exit non-zero. +- `verifyModule` runs at the end of `compile()`; invalid modules make the driver + exit non-zero without writing IR. - The FOR loop variable must be pre-DECLAREd as INTEGER; FOR reuses that alloca. The 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 @@ -83,6 +83,19 @@ mingw32-make ``` +## Usage + +```bash +cpsc test.cps # save test.ir beside the source file +cpsc test.cps -o result.ir # save IR to the specified path +lli result.ir # run the generated IR +``` + +Only one source file is supported. Options may appear before or after it. +With no arguments, `cpsc` exits silently with status 0; help text is planned. +Diagnostics go to stderr. Failed compilation does not write IR; successful +compilation overwrites an existing output file, but cannot overwrite the source. + # Note DO NOT OPEN ANY ISSUES IF YOU COMPILE WITH VISUAL STUDIO OR ANYTHING EXCEPT MINGW-W64 (EVEN WITH MINGW-W64, WINDOWS IS LIKELY TO BE A PROBLEM MAKER) diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 97765f0..ae2f015 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -95,7 +95,7 @@ public: CodeGen(); ~CodeGen(); void compile(const std::vector<std::unique_ptr<StmtAST>> &Statements); - void print(); + void print(llvm::raw_ostream &Output); bool hadError() const { return HadError; } void reportError(const char *Fmt, ...); static bool isBuiltinName(const std::string &Name); diff --git a/include/cps/Lexer.h b/include/cps/Lexer.h index 2af0260..d83e808 100644 --- a/include/cps/Lexer.h +++ b/include/cps/Lexer.h @@ -1,4 +1,5 @@ #pragma once +#include <istream> #include <string> #include <cstdint> @@ -87,9 +88,13 @@ enum Token { }; class Lexer { + std::istream &Input; + int LastChar = ' '; int CurrentLine = 1; bool HadError = false; public: + explicit Lexer(std::istream &Input) : Input(Input) {} + std::string IdentifierStr; std::string StringVal; int64_t NumVal; diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index fbf807b..903d8ef 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -539,8 +539,8 @@ void CodeGen::compile(const std::vector<std::unique_ptr<StmtAST>> &Statements) { } } -void CodeGen::print() { - TheModule->print(errs(), nullptr); +void CodeGen::print(raw_ostream &Output) { + TheModule->print(Output, nullptr); } Value *CodeGen::emitExpr(ExprAST *Expr) { diff --git a/lib/Lexer/Lexer.cc b/lib/Lexer/Lexer.cc index 4b5acae..6299371 100644 --- a/lib/Lexer/Lexer.cc +++ b/lib/Lexer/Lexer.cc @@ -8,24 +8,22 @@ using namespace cps; int Lexer::gettok() { - static int LastChar = ' '; - while (isspace(LastChar)) { if (LastChar == '\n') CurrentLine++; - LastChar = getchar(); + LastChar = Input.get(); } if (LastChar == '"') { StringVal.clear(); - LastChar = getchar(); + LastChar = Input.get(); while (LastChar != '"' && LastChar != EOF && LastChar != '\n') { StringVal += static_cast<char>(LastChar); - LastChar = getchar(); + LastChar = Input.get(); } if (LastChar == '"') { - LastChar = getchar(); + LastChar = Input.get(); } else { fprintf(stderr, "Error: Unterminated string literal at line %d\n", CurrentLine); HadError = true; @@ -35,7 +33,7 @@ int Lexer::gettok() { } if (LastChar == '\'') { - int CharCode = getchar(); + int CharCode = Input.get(); if (CharCode == EOF || CharCode == '\n') { fprintf(stderr, "Error: Unterminated char literal at line %d\n", CurrentLine); HadError = true; @@ -46,28 +44,28 @@ int Lexer::gettok() { return tok_char_literal; } - int ClosingQuote = getchar(); + int ClosingQuote = Input.get(); if (ClosingQuote != '\'') { fprintf(stderr, "Error: CHAR literal must contain exactly one character (line %d)\n", CurrentLine); HadError = true; while (ClosingQuote != EOF && ClosingQuote != '\n' && ClosingQuote != '\'') { - ClosingQuote = getchar(); + ClosingQuote = Input.get(); } CharVal = static_cast<char>(CharCode); StringVal.assign(1, CharVal); - LastChar = (ClosingQuote == '\'') ? getchar() : ClosingQuote; + LastChar = (ClosingQuote == '\'') ? Input.get() : ClosingQuote; return tok_char_literal; } CharVal = static_cast<char>(CharCode); StringVal.assign(1, CharVal); - LastChar = getchar(); + LastChar = Input.get(); return tok_char_literal; } if (isalpha(LastChar)) { IdentifierStr = static_cast<char>(LastChar); - while (isalnum((LastChar = getchar())) || LastChar == '_') + while (isalnum((LastChar = Input.get())) || LastChar == '_') IdentifierStr += static_cast<char>(LastChar); if (IdentifierStr == "DECLARE") return tok_declare; @@ -143,11 +141,11 @@ int Lexer::gettok() { bool isReal = false; do { NumStr += static_cast<char>(LastChar); - LastChar = getchar(); + LastChar = Input.get(); if (LastChar == '.' && !isReal) { isReal = true; NumStr += '.'; - LastChar = getchar(); + LastChar = Input.get(); } } while (isdigit(LastChar)); @@ -161,55 +159,55 @@ int Lexer::gettok() { } if (LastChar == '<') { - LastChar = getchar(); + LastChar = Input.get(); if (LastChar == '-') { - LastChar = getchar(); + LastChar = Input.get(); return tok_assign; } if (LastChar == '=') { - LastChar = getchar(); + LastChar = Input.get(); return tok_le; } if (LastChar == '>') { - LastChar = getchar(); + LastChar = Input.get(); return tok_ne; } return '<'; } if (LastChar == '>') { - LastChar = getchar(); + LastChar = Input.get(); if (LastChar == '=') { - LastChar = getchar(); + LastChar = Input.get(); return tok_ge; } return '>'; } if (LastChar == '=') { - LastChar = getchar(); + LastChar = Input.get(); return tok_eq; } if (LastChar == ':') { - LastChar = getchar(); + LastChar = Input.get(); return tok_colon; } - if (LastChar == '(') { LastChar = getchar(); return '('; } - if (LastChar == ')') { LastChar = getchar(); return ')'; } - if (LastChar == '[') { LastChar = getchar(); return '['; } - if (LastChar == ']') { LastChar = getchar(); return ']'; } - if (LastChar == ',') { LastChar = getchar(); return ','; } - if (LastChar == '+') { LastChar = getchar(); return '+'; } - if (LastChar == '-') { LastChar = getchar(); return '-'; } - if (LastChar == '*') { LastChar = getchar(); return '*'; } + if (LastChar == '(') { LastChar = Input.get(); return '('; } + if (LastChar == ')') { LastChar = Input.get(); return ')'; } + if (LastChar == '[') { LastChar = Input.get(); return '['; } + if (LastChar == ']') { LastChar = Input.get(); return ']'; } + if (LastChar == ',') { LastChar = Input.get(); return ','; } + if (LastChar == '+') { LastChar = Input.get(); return '+'; } + if (LastChar == '-') { LastChar = Input.get(); return '-'; } + if (LastChar == '*') { LastChar = Input.get(); return '*'; } if (LastChar == '/') { - int NextChar = getchar(); + int NextChar = Input.get(); if (NextChar == '/') { do { - LastChar = getchar(); + LastChar = Input.get(); } while (LastChar != EOF && LastChar != '\n' && LastChar != '\r'); if (LastChar != EOF) @@ -224,6 +222,6 @@ int Lexer::gettok() { return tok_eof; int ThisChar = LastChar; - LastChar = getchar(); + LastChar = Input.get(); return ThisChar; } diff --git a/tools/driver/main.cc b/tools/driver/main.cc index ca978b5..22c448c 100644 --- a/tools/driver/main.cc +++ b/tools/driver/main.cc @@ -1,17 +1,93 @@ #include "cps/Lexer.h" #include "cps/Parser.h" #include "cps/CodeGen.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/Path.h" +#include "llvm/Support/raw_ostream.h" +#include <fstream> +#include <string> -int main() { - cps::Lexer Lex; +int main(int argc, char **argv) { + if (argc == 1) { + // TODO: Print help here when help text is available. + return 0; + } + + std::string InputPath; + std::string OutputPath; + for (int I = 1; I < argc; ++I) { + std::string Arg = argv[I]; + if (Arg == "-o") { + if (!OutputPath.empty()) { + llvm::errs() << "Error: -o may only be specified once\n"; + return 1; + } + if (I + 1 == argc || argv[I + 1][0] == '-' || argv[I + 1][0] == '\0') { + llvm::errs() << "Error: -o requires an output path\n"; + return 1; + } + OutputPath = argv[++I]; + } else if (Arg.empty() || Arg[0] == '-') { + llvm::errs() << "Error: unknown argument '" << Arg << "'\n"; + return 1; + } else if (!InputPath.empty()) { + llvm::errs() << "Error: only one source file is supported\n"; + return 1; + } else { + InputPath = Arg; + } + } + + if (InputPath.empty()) { + llvm::errs() << "Error: no source file specified\n"; + return 1; + } + if (OutputPath.empty()) { + llvm::SmallString<256> DefaultPath(InputPath); + llvm::sys::path::replace_extension(DefaultPath, ".ir"); + OutputPath = std::string(DefaultPath); + } + + std::ifstream Input(InputPath, std::ios::binary); + if (!Input) { + llvm::errs() << "Error: cannot open source file '" << InputPath << "'\n"; + return 1; + } + if (llvm::sys::fs::equivalent(InputPath, OutputPath)) { + llvm::errs() << "Error: output file must not be the source file\n"; + return 1; + } + + cps::Lexer Lex(Input); cps::Parser Parser(Lex); auto Statements = Parser.Parse(); + if (Input.bad()) { + llvm::errs() << "Error: cannot read source file '" << InputPath << "'\n"; + return 1; + } + if (Lex.hadError() || Parser.hadError()) return 1; cps::CodeGen CG; CG.compile(Statements); - CG.print(); + if (CG.hadError()) return 1; - return (Lex.hadError() || Parser.hadError() || CG.hadError()) ? 1 : 0; + std::error_code EC; + llvm::raw_fd_ostream Output(OutputPath, EC); + if (EC) { + llvm::errs() << "Error: cannot open output file '" << OutputPath + << "': " << EC.message() << "\n"; + return 1; + } + CG.print(Output); + Output.close(); + if (Output.has_error()) { + llvm::errs() << "Error: cannot write output file '" << OutputPath + << "': " << Output.error().message() << "\n"; + Output.clear_error(); + return 1; + } + return 0; } |
