diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-18 08:04:15 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-18 08:04:15 +0000 |
| commit | 927e3fc0384c941f9ad313a28768660c3cdb1777 (patch) | |
| tree | ee82bef8d8332dad79d05db87f33979f0e7eb423 | |
| parent | 34a4998658674411ef3b5a392483990467a5d6e2 (diff) | |
feat(driver): add #LIB directive hiding library lines in runtime errors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | AGENTS/ARCH.md | 2 | ||||
| -rw-r--r-- | AGENTS/CONVENTIONS.md | 2 | ||||
| -rw-r--r-- | doc/language/cpsc-language-overview.7.md | 2 | ||||
| -rw-r--r-- | doc/language/keyword/cpsc-keyword-lib.7.md | 31 | ||||
| -rw-r--r-- | include/cps/FunctionGen.h | 2 | ||||
| -rw-r--r-- | include/cps/Lexer.h | 3 | ||||
| -rw-r--r-- | include/cps/RuntimeCheck.h | 6 | ||||
| -rw-r--r-- | include/cps/SourceUnit.h | 1 | ||||
| -rw-r--r-- | lib/CodeGen/CodeGen.cc | 5 | ||||
| -rw-r--r-- | lib/CodeGen/FileHandler.cc | 10 | ||||
| -rw-r--r-- | lib/CodeGen/FunctionGen.cc | 1 | ||||
| -rw-r--r-- | lib/CodeGen/MemoryHandler.cc | 8 | ||||
| -rw-r--r-- | lib/CodeGen/RuntimeCheck.cc | 17 | ||||
| -rw-r--r-- | lib/Lexer/Lexer.cc | 18 | ||||
| -rw-r--r-- | tests/i/library-lines/case.json | 1 | ||||
| -rw-r--r-- | tests/i/library-lines/expected-stderr.txt | 4 | ||||
| -rw-r--r-- | tests/i/library-lines/expected.txt | 4 | ||||
| -rw-r--r-- | tests/i/library-lines/library.cps | 25 | ||||
| -rw-r--r-- | tests/i/library-lines/program.cps | 21 | ||||
| -rw-r--r-- | tests/iii/unknown-directive/case.json | 1 | ||||
| -rw-r--r-- | tests/iii/unknown-directive/program.cps | 1 | ||||
| -rw-r--r-- | tools/driver/main.cc | 2 |
22 files changed, 153 insertions, 14 deletions
diff --git a/AGENTS/ARCH.md b/AGENTS/ARCH.md index 1755fd0..2e2ec2d 100644 --- a/AGENTS/ARCH.md +++ b/AGENTS/ARCH.md @@ -353,3 +353,5 @@ they are pure data with virtual destructors only — no codegen/visitor methods. See [MAN.md](MAN.md) before editing the command or syntax manuals. It defines source layout, naming, prose formatting and the small checks needed for changes. + +**Library units.** Line-leading `#LIB` marks SourceUnit::IsLibrary. RuntimeCheck uses the externally visible i32 `__cps_user_line`; FunctionGen’s shared call hook updates it only on non-library to library calls. Runtime helper line arguments use RuntimeCheck::lineValue too. Compiler diagnostics retain the owning file location. diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md index 598ff76..fa9f12a 100644 --- a/AGENTS/CONVENTIONS.md +++ b/AGENTS/CONVENTIONS.md @@ -209,3 +209,5 @@ __cps_main with internal linkage. The generated i32 main wrapper calls it throug ExceptionHandler, truncates its result and retains the final uncaught handler. Identifiers may begin with an underscore. The case-insensitive `__cps_` prefix is reserved in every user identifier, including EXTERN declarations. + +**Library units.** Line-leading `#LIB` marks SourceUnit::IsLibrary. RuntimeCheck uses the externally visible i32 `__cps_user_line`; FunctionGen’s shared call hook updates it only on non-library to library calls. Runtime helper line arguments use RuntimeCheck::lineValue too. Compiler diagnostics retain the owning file location. diff --git a/doc/language/cpsc-language-overview.7.md b/doc/language/cpsc-language-overview.7.md index 96fb071..f04e044 100644 --- a/doc/language/cpsc-language-overview.7.md +++ b/doc/language/cpsc-language-overview.7.md @@ -24,6 +24,8 @@ The compiler extensions TRY/CATCH/ENDTRY and THROW support anonymous exceptions Multi-file builds use the compiler extensions INCLUDE and GLOBAL. Pass every source file on the command line and include the files whose definitions are needed. The entry file contains the sole top-level executable code, or a parameterless FUNCTION MAIN returning INTEGER; definitions alone do not form a runnable program. +#LIB marks a library source unit. A line-leading `#` starts a compiler directive, not a comment; unknown directives are errors. Library runtime diagnostics report the first user call site. See [cpsc-keyword-lib(7)](keyword/cpsc-keyword-lib.7.md). + # KEYWORDS - [cpsc-keyword-include(7)](keyword/cpsc-keyword-include.7.md): import definitions from another compilation unit. diff --git a/doc/language/keyword/cpsc-keyword-lib.7.md b/doc/language/keyword/cpsc-keyword-lib.7.md new file mode 100644 index 0000000..46af0a4 --- /dev/null +++ b/doc/language/keyword/cpsc-keyword-lib.7.md @@ -0,0 +1,31 @@ +% CPSC-KEYWORD-LIB(7) + +# NAME + +cpsc-keyword-lib - mark a library compilation unit + +# SYNOPSIS + +```text +#LIB +``` + +# DESCRIPTION + +A line-leading `#` (optionally preceded by whitespace) begins a compiler directive. Only `#LIB` is supported. It marks the entire source file as library code. Compile-time diagnostics retain library locations; runtime checks use the line of the first call from user code into library code, including methods, operators, automatic constructors/destructors and EXTERN calls. Library-to-library calls preserve that line. Directives are not comments. + +C++ library objects may read `extern "C" int __cps_user_line`, print `[Fatal] line N: reason` to stderr and throw an anonymous integer exception for pseudocode TRY/CATCH. The global is shared by the program and is not thread-local. + +# EXAMPLE + +```text +#LIB +PROCEDURE Check() + DECLARE A : ARRAY[1:1] OF INTEGER + OUTPUT A[2] +ENDPROCEDURE +``` + +# SEE ALSO + +[cpsc-keyword-include(7)](cpsc-keyword-include.7.md), [cpsc-keyword-try(7)](cpsc-keyword-try.7.md). diff --git a/include/cps/FunctionGen.h b/include/cps/FunctionGen.h index 5eb0f82..57a27ff 100644 --- a/include/cps/FunctionGen.h +++ b/include/cps/FunctionGen.h @@ -79,6 +79,7 @@ private: std::map<size_t, std::string> UnitPaths; size_t CurrentUnit = 0; std::function<bool(size_t)> Visible; + std::function<void(const FuncSig &)> BeforeCall; ArrayParamBinder BindArrayParam; std::function<void(llvm::Value *, const std::string &, int)> RegisterCopy; @@ -103,6 +104,7 @@ public: } void setCurrentUnit(size_t Unit, const std::string &Path) { CurrentUnit = Unit; UnitPaths[Unit] = Path; } + void setCallHook(std::function<void(const FuncSig &)> Hook) { BeforeCall = std::move(Hook); } void setVisibility(std::function<bool(size_t)> Predicate) { Visible = std::move(Predicate); } std::vector<const FuncSig *> getOverloads(const std::string &Name) const; bool hasFunction(const std::string &Name) const { return !getOverloads(Name).empty(); } diff --git a/include/cps/Lexer.h b/include/cps/Lexer.h index 1c4723e..588a5fa 100644 --- a/include/cps/Lexer.h +++ b/include/cps/Lexer.h @@ -120,6 +120,8 @@ class Lexer { int CurrentLine = 1; int TokenLine = 1; bool HadError = false; + bool Library = false; + bool LineStart = true; struct TokenData { int Kind, Line; std::string Identifier, String; @@ -145,6 +147,7 @@ public: int peekToken(unsigned Offset = 0); int peekLine(unsigned Offset = 0); int getLine() const { return TokenLine; } + bool isLibrary() const { return Library; } bool hadError() const { return HadError; } }; diff --git a/include/cps/RuntimeCheck.h b/include/cps/RuntimeCheck.h index 30a1910..d2ee1ea 100644 --- a/include/cps/RuntimeCheck.h +++ b/include/cps/RuntimeCheck.h @@ -18,6 +18,8 @@ class RuntimeCheck { llvm::FunctionCallee StderrFunc; // Windows: __acrt_iob_func(2) llvm::GlobalVariable *StderrVar; // elsewhere: the C library's stderr ExceptionHandler &Exceptions; + bool LibraryMode = false; + llvm::GlobalVariable *UserLine; llvm::Value *DivZeroMsg; llvm::Value *OutOfBoundsMsg; llvm::Value *EnumRangeMsg; @@ -31,6 +33,10 @@ public: RuntimeCheck(llvm::Module &M, llvm::LLVMContext &C, llvm::IRBuilder<> &B, ExceptionHandler &EH); + void setLibraryMode(bool Enabled) { LibraryMode = Enabled; } + llvm::Value *lineValue(int Line); + void setUserLine(int Line); + // Format a runtime diagnostic (printf-style) and write it to stderr. void emitStderr(llvm::Value *Msg, llvm::ArrayRef<llvm::Value *> FmtArgs = {}); diff --git a/include/cps/SourceUnit.h b/include/cps/SourceUnit.h index db9ad30..34266ac 100644 --- a/include/cps/SourceUnit.h +++ b/include/cps/SourceUnit.h @@ -9,5 +9,6 @@ struct SourceUnit { std::string Path; const std::vector<std::unique_ptr<StmtAST>> &Statements; std::vector<bool> Visible; + bool IsLibrary = false; }; } diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index f7c4de6..c9d8c1a 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -161,6 +161,11 @@ void CodeGen::selectUnit(size_t Unit) { CurrentUnit = Unit; DiagnosticSource = Units->size() > 1 ? (*Units)[Unit].Path : ""; FuncGen->setCurrentUnit(Unit, (*Units)[Unit].Path); + RuntimeChecker->setLibraryMode((*Units)[Unit].IsLibrary); + FuncGen->setCallHook([this](const FuncSig &Sig) { + if (!(*Units)[CurrentUnit].IsLibrary && (*Units)[Sig.OwnerUnit].IsLibrary) + RuntimeChecker->setUserLine(DiagnosticLine); + }); } void CodeGen::importSymbols() { diff --git a/lib/CodeGen/FileHandler.cc b/lib/CodeGen/FileHandler.cc index a4d7561..a7879e0 100644 --- a/lib/CodeGen/FileHandler.cc +++ b/lib/CodeGen/FileHandler.cc @@ -246,31 +246,31 @@ void FileHandler::emitOpen(Value *NameStr, FileMode Mode, int Line) { ensureRuntime(); Exceptions.emitCall(OpenFn, {NameStr, ConstantInt::get(Type::getInt8Ty(Context), uint8_t(Mode)), - ConstantInt::get(Context, APInt(32, Line))}); + Checker.lineValue(Line)}); } Value *FileHandler::emitRead(Value *NameStr, int Line) { if (!NameStr) return nullptr; ensureRuntime(); - return Exceptions.emitCall(ReadFn, {NameStr, ConstantInt::get(Context, APInt(32, Line))}, + return Exceptions.emitCall(ReadFn, {NameStr, Checker.lineValue(Line)}, "file_line"); } void FileHandler::emitWrite(Value *NameStr, Value *DataStr, int Line) { if (!NameStr || !DataStr) return; ensureRuntime(); - Exceptions.emitCall(WriteFn, {NameStr, DataStr, ConstantInt::get(Context, APInt(32, Line))}); + Exceptions.emitCall(WriteFn, {NameStr, DataStr, Checker.lineValue(Line)}); } void FileHandler::emitClose(Value *NameStr, int Line) { if (!NameStr) return; ensureRuntime(); - Exceptions.emitCall(CloseFn, {NameStr, ConstantInt::get(Context, APInt(32, Line))}); + Exceptions.emitCall(CloseFn, {NameStr, Checker.lineValue(Line)}); } Value *FileHandler::emitEof(Value *NameStr, int Line) { if (!NameStr) return nullptr; ensureRuntime(); - return Exceptions.emitCall(EofFn, {NameStr, ConstantInt::get(Context, APInt(32, Line))}, + return Exceptions.emitCall(EofFn, {NameStr, Checker.lineValue(Line)}, "file_eof"); } diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc index bccf397..a0d0e32 100644 --- a/lib/CodeGen/FunctionGen.cc +++ b/lib/CodeGen/FunctionGen.cc @@ -451,6 +451,7 @@ llvm::Value *FunctionGen::emitCall(const std::string &Callee, const std::vector< } llvm::Value *FunctionGen::emitCall(const FuncSig &Sig, const std::vector<llvm::Value*> &Args) { + if (BeforeCall) BeforeCall(Sig); if (!Sig.IsExternal) return emitCall(Sig.LinkName, Args); FunctionCallee Callee(Sig.ExternType, Module.getNamedValue(Sig.LinkName)); Value *Call = Sig.ExternType->getReturnType()->isVoidTy() diff --git a/lib/CodeGen/MemoryHandler.cc b/lib/CodeGen/MemoryHandler.cc index fa25eb9..7cb62b3 100644 --- a/lib/CodeGen/MemoryHandler.cc +++ b/lib/CodeGen/MemoryHandler.cc @@ -114,12 +114,12 @@ void MemoryHandler::buildRelease() { Value *MemoryHandler::emitAllocate(Value *Size, int Line) { ensureRuntime(); - return Exceptions.emitCall(AllocateFn, {Size, Builder.getInt32(Line)}, "allocated"); + return Exceptions.emitCall(AllocateFn, {Size, Checker.lineValue(Line)}, "allocated"); } Value *MemoryHandler::emitRelease(Value *Address, int Line) { ensureRuntime(); - return Exceptions.emitCall(ReleaseFn, {Address, Builder.getInt32(Line)}, "released"); + return Exceptions.emitCall(ReleaseFn, {Address, Checker.lineValue(Line)}, "released"); } void MemoryHandler::buildResize() { @@ -150,10 +150,10 @@ void MemoryHandler::buildResize() { Value *MemoryHandler::emitResize(Value *Address, Value *Size, int Line) { ensureRuntime(); - return Exceptions.emitCall(ResizeFn, {Address, Size, Builder.getInt32(Line)}, "resized"); + return Exceptions.emitCall(ResizeFn, {Address, Size, Checker.lineValue(Line)}, "resized"); } void MemoryHandler::emitValidate(Value *Address, int Line) { ensureRuntime(); - Exceptions.emitCall(FindFn, {Address, Builder.getInt32(Line)}); + Exceptions.emitCall(FindFn, {Address, Checker.lineValue(Line)}); } diff --git a/lib/CodeGen/RuntimeCheck.cc b/lib/CodeGen/RuntimeCheck.cc index 0515f1e..4cee16f 100644 --- a/lib/CodeGen/RuntimeCheck.cc +++ b/lib/CodeGen/RuntimeCheck.cc @@ -11,9 +11,20 @@ using namespace cps; RuntimeCheck::RuntimeCheck(Module &M, LLVMContext &C, IRBuilder<> &B, ExceptionHandler &EH) : TheModule(M), TheContext(C), Builder(B), Exceptions(EH) { + UserLine = new GlobalVariable(TheModule, Builder.getInt32Ty(), false, + GlobalValue::ExternalLinkage, Builder.getInt32(0), "__cps_user_line"); setupExternalFunctions(); } +Value *RuntimeCheck::lineValue(int Line) { + return LibraryMode ? Builder.CreateLoad(Builder.getInt32Ty(), UserLine, "user_line") + : static_cast<Value *>(Builder.getInt32(Line)); +} + +void RuntimeCheck::setUserLine(int Line) { + Builder.CreateStore(Builder.getInt32(Line), UserLine); +} + void RuntimeCheck::setupExternalFunctions() { Type *PtrTy = PointerType::getUnqual(TheContext); Type *I32 = Type::getInt32Ty(TheContext); @@ -54,7 +65,7 @@ void RuntimeCheck::emitStderr(Value *Msg, ArrayRef<Value *> FmtArgs) { } void RuntimeCheck::emitCheck(Value *Condition, Value *Msg, int Line) { - emitCheck(Condition, Msg, {ConstantInt::get(TheContext, APInt(32, Line))}); + emitCheck(Condition, Msg, {lineValue(Line)}); } void RuntimeCheck::emitCheck(Value *Condition, Value *Msg, ArrayRef<Value *> FmtArgs) { @@ -105,7 +116,7 @@ void RuntimeCheck::emitEnumRangeCheck(Value *Ordinal, uint64_t ValueCount, Value *NameStr = Builder.CreateGlobalString(TypeName, "enum_name", 0, &TheModule); emitCheck(OutOfRange, EnumRangeMsg, - {ConstantInt::get(TheContext, APInt(32, Line)), NameStr}); + {lineValue(Line), NameStr}); } void RuntimeCheck::emitNullDerefCheck(Value *Ptr, int Line) { @@ -125,6 +136,6 @@ void RuntimeCheck::emitArrayArgBoundsCheck(Value *ActualLower, Value *ActualUppe Value *Mismatch = Builder.CreateOr(LoDiff, HiDiff, "arg_bounds_ne"); emitCheck(Mismatch, ArrayArgBoundsMsg, - {ConstantInt::get(TheContext, APInt(32, Line)), + {lineValue(Line), ActualLower, ActualUpper, DeclLo, DeclHi}); } diff --git a/lib/Lexer/Lexer.cc b/lib/Lexer/Lexer.cc index 9435d04..577a741 100644 --- a/lib/Lexer/Lexer.cc +++ b/lib/Lexer/Lexer.cc @@ -91,11 +91,27 @@ std::string Lexer::readEscape(bool IsChar) { int Lexer::readToken() { while (isspace(LastChar)) { - if (LastChar == '\n') CurrentLine++; + if (LastChar == '\n') { CurrentLine++; LineStart = true; } LastChar = Input.get(); } TokenLine = CurrentLine; + if (LastChar == '#' && LineStart) { + std::string Directive; + do { + Directive += static_cast<char>(LastChar); + LastChar = Input.get(); + } while (LastChar != EOF && LastChar != '\n' && LastChar != '\r'); + while (!Directive.empty() && isspace(static_cast<unsigned char>(Directive.back()))) + Directive.pop_back(); + if (Directive == "#LIB") Library = true; + else { + cps::sourceDiagnostic(TokenLine, "error: Unknown compiler directive at line %d\n", TokenLine); + HadError = true; + } + return readToken(); + } + LineStart = false; if (LastChar == '"' || LastChar == '\'') { const int Quote = LastChar; diff --git a/tests/i/library-lines/case.json b/tests/i/library-lines/case.json new file mode 100644 index 0000000..48c69f4 --- /dev/null +++ b/tests/i/library-lines/case.json @@ -0,0 +1 @@ +{"sources":["program.cps","library.cps"]} diff --git a/tests/i/library-lines/expected-stderr.txt b/tests/i/library-lines/expected-stderr.txt new file mode 100644 index 0000000..c58dca5 --- /dev/null +++ b/tests/i/library-lines/expected-stderr.txt @@ -0,0 +1,4 @@ +[Fatal] line 3: Array index out of bounds +[Fatal] line 8: Array argument bounds [1:3] do not match the declared parameter bounds [1:2] +[Fatal] line 13: File 'unopened' is not open +[Fatal] line 18: Division by zero diff --git a/tests/i/library-lines/expected.txt b/tests/i/library-lines/expected.txt new file mode 100644 index 0000000..a2e855d --- /dev/null +++ b/tests/i/library-lines/expected.txt @@ -0,0 +1,4 @@ +bounds caught +contract caught +file caught +constructor caught diff --git a/tests/i/library-lines/library.cps b/tests/i/library-lines/library.cps new file mode 100644 index 0000000..a35a2e9 --- /dev/null +++ b/tests/i/library-lines/library.cps @@ -0,0 +1,25 @@ +#LIB +PROCEDURE Check() + CALL Inner() +ENDPROCEDURE +PROCEDURE Inner() + DECLARE A : ARRAY[1:1] OF INTEGER + OUTPUT A[2] +ENDPROCEDURE +PROCEDURE Contract(A : ARRAY[1:2] OF INTEGER) +ENDPROCEDURE +PROCEDURE Bounds() + DECLARE L : INTEGER + L <- 3 + DECLARE A : ARRAY[1:L] OF INTEGER + CALL Contract(A) +ENDPROCEDURE +PROCEDURE FileError() + CLOSEFILE "unopened" +ENDPROCEDURE +CLASS Broken + PROCEDURE NEW() + DECLARE X : INTEGER + X <- 1 DIV 0 + ENDPROCEDURE +ENDCLASS diff --git a/tests/i/library-lines/program.cps b/tests/i/library-lines/program.cps new file mode 100644 index 0000000..62a7675 --- /dev/null +++ b/tests/i/library-lines/program.cps @@ -0,0 +1,21 @@ +INCLUDE "library.cps" +TRY + CALL Check() +CATCH + OUTPUT "bounds caught" +ENDTRY +TRY + CALL Bounds() +CATCH + OUTPUT "contract caught" +ENDTRY +TRY + CALL FileError() +CATCH + OUTPUT "file caught" +ENDTRY +TRY + DECLARE X : Broken +CATCH + OUTPUT "constructor caught" +ENDTRY diff --git a/tests/iii/unknown-directive/case.json b/tests/iii/unknown-directive/case.json new file mode 100644 index 0000000..d593f34 --- /dev/null +++ b/tests/iii/unknown-directive/case.json @@ -0,0 +1 @@ +{"errors":[{"reason":"Unknown compiler directive","line":1}]} diff --git a/tests/iii/unknown-directive/program.cps b/tests/iii/unknown-directive/program.cps new file mode 100644 index 0000000..1def142 --- /dev/null +++ b/tests/iii/unknown-directive/program.cps @@ -0,0 +1 @@ +#FOO diff --git a/tools/driver/main.cc b/tools/driver/main.cc index 3ee221b..5f3aaa2 100644 --- a/tools/driver/main.cc +++ b/tools/driver/main.cc @@ -161,7 +161,7 @@ int main(int argc, char **argv) { return compilationTerminated(); } if (Lex.hadError() || Parser.hadError()) return compilationTerminated(); - Units.push_back({InputPath, Programs[I], std::vector<bool>(InputPaths.size(), false)}); + Units.push_back({InputPath, Programs[I], std::vector<bool>(InputPaths.size(), false), Lex.isLibrary()}); Units.back().Visible[I] = true; } for (size_t I = 0; I < Units.size(); ++I) { |
