aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-13 03:23:52 +0000
committer_ov4 <ov4@ov4.org>2026-10-13 03:23:52 +0000
commitf9a0c7e9687bc3e113f4c8f44861533c9ca48b3a (patch)
tree5d51c5a403dd096b0c3748118409be20eb3e1900
parent3818460a473bd3ffcd35f09eb6429e1b0f00bc07 (diff)
feat: compile source files with configurable IR output
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
-rw-r--r--AGENTS/ARCH.md25
-rw-r--r--AGENTS/CONVENTIONS.md6
-rw-r--r--AGENTS/PITFALLS.md4
-rw-r--r--README.md13
-rw-r--r--include/cps/CodeGen.h2
-rw-r--r--include/cps/Lexer.h5
-rw-r--r--lib/CodeGen/CodeGen.cc4
-rw-r--r--lib/Lexer/Lexer.cc64
-rw-r--r--tools/driver/main.cc84
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
diff --git a/README.md b/README.md
index dd08059..b251eba 100644
--- a/README.md
+++ b/README.md
@@ -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;
}