aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-07-26 12:07:51 +0000
committer_ov4 <ov4@ov4.org>2026-07-26 12:07:51 +0000
commit4a25b279d8f6c662fa8152cfe42e038d1ea06901 (patch)
tree58223f2db133a10827e0a6d0c18c33f04bbc2527
parentefc86b5c7625cf4c7074319f42f68807a25bde9d (diff)
refactor: keep pseudocode signatures, single call-marshalling path
emitPrototype now records each FUNCTION/PROCEDURE's pseudocode signature (FuncSig: return type + per-param type/BYREF flag). CodeGen:: marshalCallArgs replaces the two verbatim-duplicated marshalling blocks and dispatches on the declared signature instead of inferring BYREF from pointer-typed LLVM params: BYVAL STRING params now receive the string data pointer (no more runtime garbage), and BYVAL args are coerced to the declared type (INTEGER->REAL args no longer emit invalid IR). Top-level prototypes are pre-registered in compile(), so calls may precede definitions; GenerateCall no longer fabricates i64 externs for unknown names - an unregistered callee is a compile error. OUTPUT reports an error when it cannot infer the expression type instead of printing with the INTEGER format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
-rw-r--r--AGENTS/CONVENTIONS.md19
-rw-r--r--AGENTS/PITFALLS.md3
-rw-r--r--AGENTS/SUBSYSTEMS.md21
-rw-r--r--include/cps/CodeGen.h3
-rw-r--r--include/cps/FunctionGen.h12
-rw-r--r--lib/CodeGen/CodeGen.cc112
-rw-r--r--lib/CodeGen/FunctionGen.cc24
7 files changed, 107 insertions, 87 deletions
diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md
index c05734f..e09b6ee 100644
--- a/AGENTS/CONVENTIONS.md
+++ b/AGENTS/CONVENTIONS.md
@@ -58,16 +58,15 @@ non-zero when any flag is set. The parser still drops a failed statement, skips
token, and continues — later statements can be swallowed during recovery, but the
failure is always diagnosed and reflected in the exit code.
-**BYREF is a pointer-typed parameter, inferred structurally at call sites.**
-`FunctionGen` emits pointer params for BYREF; call sites (in CodeGen, duplicated for
-call-expr and call-stmt) treat *any* pointer-typed callee param as BYREF and require the
-argument to be a bare variable whose alloca is passed. The callee side, however, binds
-by the *declared* flag. Since STRING itself lowers to `ptr`, the two sides disagree for
-STRING: the caller always passes the alloca, but a declared-BYVAL STRING param (the
-default) copies that alloca *address* and treats it as the string data pointer — reading
-garbage at runtime. Only declared-BYREF STRING parameters work, and STRING arguments can
-never be literals/expressions. Function names get no mangling — a pseudocode
-`FUNCTION printf` collides with libc.
+**Calls dispatch on the declared signature.** `FunctionGen::emitPrototype` records every
+function's pseudocode signature (`FuncSig`: return type name + per-param type name and
+BYREF flag); `CodeGen::marshalCallArgs` — the single call-marshalling path for both
+call-expr and call-stmt — requires BYREF arguments to be bare variables (their alloca is
+passed) and coerces BYVAL arguments to the declared parameter type. STRING works on both
+sides in both modes. Top-level prototypes are pre-registered before statement emission,
+so call-before-definition is fine; a call with no registered signature is a compile
+error. Function names get no mangling — a pseudocode `FUNCTION printf` collides with
+libc.
**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
diff --git a/AGENTS/PITFALLS.md b/AGENTS/PITFALLS.md
index b068dee..393f507 100644
--- a/AGENTS/PITFALLS.md
+++ b/AGENTS/PITFALLS.md
@@ -20,8 +20,7 @@ test programs.
bits.
- **`getExprTypeInfo` returns nullptr** for expressions it can't classify (null-check it
at any new call site) and internally defaults to INTEGER for arithmetic binaries and
- unknown-function calls; OUTPUT substitutes INTEGER for a nullptr result — so wrong
- inference prints garbage instead of erroring.
+ 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
diff --git a/AGENTS/SUBSYSTEMS.md b/AGENTS/SUBSYSTEMS.md
index 2910bb3..f26cddc 100644
--- a/AGENTS/SUBSYSTEMS.md
+++ b/AGENTS/SUBSYSTEMS.md
@@ -6,9 +6,10 @@ known bugs: `PITFALLS.md`.)
**Arrays** (`ArrayHandler`): a DECLAREd array is one malloc'd flat buffer (memset to
zero), row-major, any rank, any bounds. Bounds and multipliers are *compile-time SSA
values* cached in `ArrayMetadata` (global `ArrayTable` map, keyed by name). Every access
-goes through `getMetadata` → per-dimension `emitIndexCheck` → `computeFlatIndex` →
-`getElementPointer`; never hand-roll indexing. STRING elements read as null until
-written; every read path must replicate the null-guard `select` to the empty string.
+goes through `emitCheckedIndices` (validates the index count for the caller's semantics,
+then per-dimension coerce + `emitIndexCheck`) → `computeFlatIndex` → `loadElement`
+(load + the STRING null-guard `select` to the empty string); never hand-roll indexing
+or element loads.
`OUTPUT arr` (and partially-indexed accesses) is intercepted by
`ArrayHandler::tryEmitArrayOutput` — the first branch of OUTPUT dispatch, emitting
nested print loops — so any rework of OUTPUT must preserve that interception.
@@ -17,12 +18,14 @@ nested print loops — so any rework of OUTPUT must preserve that interception.
name. Params are `tuple<name, typeString, isByRef>`. BYVAL args are copied into
entry-block allocas; BYREF args bind the incoming pointer directly as the variable's
storage. Missing RETURNS defaults the return type to INTEGER; procedures use "VOID".
-**Definitions must precede first call**: calling a not-yet-defined function fabricates
-an extern declaration (i64 return, param types guessed from the arguments), and when the
-real definition arrives, `emitFunctionDef` reuses that declaration by name without
-checking the signature — the body attaches to the wrong prototype and the module is
-invalid. Body emission is inverted: CodeGen injects a `StmtEmitter` callback, and must
-restore its own insert point afterwards.
+`emitPrototype` records each function's pseudocode signature in a `FuncSig` map
+(`FunctionGen::getSignature`); `CodeGen::marshalCallArgs` dispatches call arguments on
+those *declared* param types and BYREF flags (BYVAL args are coerced to the declared
+type), and `getExprTypeInfo` reads return types from the same map. Top-level prototypes
+are pre-registered at the start of `compile()`, so calls may precede definitions;
+calling a name with no signature is a compile error (no extern is fabricated). Body
+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
diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h
index 268ba7e..b1d3831 100644
--- a/include/cps/CodeGen.h
+++ b/include/cps/CodeGen.h
@@ -69,6 +69,9 @@ class CodeGen {
const SymbolInfo *getSymbolInfo(const std::string &Name) const;
const TypeInfo *getExprTypeInfo(ExprAST *Expr) const;
llvm::Value *coerceValueToType(llvm::Value *Val, const TypeInfo *TargetInfo);
+ bool marshalCallArgs(const std::string &Callee,
+ const std::vector<std::unique_ptr<ExprAST>> &ArgExprs,
+ std::vector<llvm::Value*> &Out);
void emitDeclareStmt(DeclareStmtAST *Stmt);
void emitOutputValue(llvm::Value *Val, const TypeInfo *TypeInfo, bool AppendNewline = true);
diff --git a/include/cps/FunctionGen.h b/include/cps/FunctionGen.h
index 1dc3901..f55db1b 100644
--- a/include/cps/FunctionGen.h
+++ b/include/cps/FunctionGen.h
@@ -11,6 +11,14 @@
namespace cps {
+// Pseudocode-level signature of a FUNCTION/PROCEDURE, kept so call sites can
+// dispatch on declared parameter types and BYREF flags instead of inferring
+// them back from LLVM types.
+struct FuncSig {
+ std::string ReturnTypeName;
+ std::vector<std::pair<std::string, bool>> Params; // (TypeName, IsByRef)
+};
+
class FunctionGen {
llvm::LLVMContext &Context;
llvm::Module &Module;
@@ -21,6 +29,8 @@ class FunctionGen {
std::map<std::string, SymbolInfo> &Symbols;
bool &HadError;
+ std::map<std::string, FuncSig> Signatures;
+
void createArgumentAllocas(llvm::Function *F, const std::vector<std::tuple<std::string, std::string, bool>> &Args);
public:
@@ -35,6 +45,8 @@ public:
llvm::Type *getLLVMType(const std::string &TypeName);
+ const FuncSig *getSignature(const std::string &Name) const;
+
llvm::Function *emitPrototype(PrototypeAST *Proto);
llvm::Function *emitFunctionDef(FunctionDefAST *FuncAST, const std::function<void(StmtAST*)> &StmtEmitter);
diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc
index 581e4a9..1c019d9 100644
--- a/lib/CodeGen/CodeGen.cc
+++ b/lib/CodeGen/CodeGen.cc
@@ -211,6 +211,9 @@ const TypeInfo *CodeGen::getExprTypeInfo(ExprAST *Expr) const {
if (const BuiltinInfo *B = findBuiltin(Name)) {
return resolveType(B->ReturnTypeName);
}
+ if (const FuncSig *Sig = FuncGen->getSignature(Name)) {
+ return resolveType(Sig->ReturnTypeName);
+ }
Function *CalleeF = TheModule->getFunction(Name);
if (!CalleeF) return resolveType("INTEGER");
@@ -416,6 +419,14 @@ void CodeGen::compile(const std::vector<std::unique_ptr<StmtAST>> &Statements) {
BasicBlock *BB = BasicBlock::Create(*TheContext, "entry", F);
Builder->SetInsertPoint(BB);
+ // Pre-register every top-level FUNCTION/PROCEDURE prototype so calls may
+ // precede definitions.
+ for (const auto &Stmt : Statements) {
+ if (auto *FuncDef = dynamic_cast<FunctionDefAST*>(Stmt.get())) {
+ FuncGen->emitPrototype(FuncDef->getProto());
+ }
+ }
+
for (const auto &Stmt : Statements) {
emitStmt(Stmt.get());
}
@@ -578,39 +589,54 @@ Value *CodeGen::emitExpr(ExprAST *Expr) {
return StrConvHandler->emitStrToNum(emitExpr(Call->getArgs()[0].get()), true);
}
- Function *CalleeF = TheModule->getFunction(Call->getCallee());
std::vector<Value*> Args;
+ if (!marshalCallArgs(Name, Call->getArgs(), Args)) return nullptr;
+ return FuncGen->emitCallExpr(Call, Args);
+ }
- for (unsigned i = 0; i < Call->getArgs().size(); ++i) {
- ExprAST *ArgExpr = Call->getArgs()[i].get();
- bool IsByRef = false;
+ return nullptr;
+}
- if (CalleeF && i < CalleeF->arg_size()) {
- if (CalleeF->getArg(i)->getType()->isPointerTy()) {
- IsByRef = true;
- }
- }
+// Collects the arguments for a call to a user FUNCTION/PROCEDURE, dispatching
+// on the *declared* signature: BYREF parameters take the variable's alloca,
+// BYVAL parameters are evaluated and coerced to the declared parameter type.
+bool CodeGen::marshalCallArgs(const std::string &Callee,
+ const std::vector<std::unique_ptr<ExprAST>> &ArgExprs,
+ std::vector<Value*> &Out) {
+ const FuncSig *Sig = FuncGen->getSignature(Callee);
+ if (!Sig) {
+ reportError("Call to undefined function %s", Callee.c_str());
+ return false;
+ }
+ if (ArgExprs.size() != Sig->Params.size()) {
+ reportError("Incorrect # arguments passed to %s", Callee.c_str());
+ return false;
+ }
- if (IsByRef) {
- if (auto *Var = dynamic_cast<VariableExprAST*>(ArgExpr)) {
- Value *Ptr = getNamedValue(Var->getName());
- if (!Ptr) {
- reportError("Unknown variable %s in BYREF call", Var->getName().c_str());
- return nullptr;
- }
- Args.push_back(Ptr);
- } else {
- reportError("BYREF argument must be a variable.");
- return nullptr;
- }
- } else {
- Args.push_back(emitExpr(ArgExpr));
+ for (size_t i = 0; i < ArgExprs.size(); ++i) {
+ const std::string &TypeName = Sig->Params[i].first;
+ bool IsByRef = Sig->Params[i].second;
+ ExprAST *ArgExpr = ArgExprs[i].get();
+
+ if (IsByRef) {
+ auto *Var = dynamic_cast<VariableExprAST*>(ArgExpr);
+ if (!Var) {
+ reportError("BYREF argument to %s must be a variable", Callee.c_str());
+ return false;
+ }
+ Value *Ptr = getNamedValue(Var->getName());
+ if (!Ptr) {
+ reportError("Unknown variable %s in BYREF call", Var->getName().c_str());
+ return false;
}
+ Out.push_back(Ptr);
+ } else {
+ Value *V = coerceValueToType(emitExpr(ArgExpr), resolveType(TypeName));
+ if (!V) return false;
+ Out.push_back(V);
}
- return FuncGen->emitCallExpr(Call, Args);
}
-
- return nullptr;
+ return true;
}
void CodeGen::emitIfStmt(IfStmtAST *Stmt) {
@@ -775,35 +801,8 @@ void CodeGen::emitStmt(StmtAST *Stmt) {
}
if (auto *Call = dynamic_cast<CallStmtAST*>(Stmt)) {
- Function *CalleeF = TheModule->getFunction(Call->getCallee());
std::vector<Value*> Args;
-
- for (unsigned i = 0; i < Call->getArgs().size(); ++i) {
- ExprAST *ArgExpr = Call->getArgs()[i].get();
- bool IsByRef = false;
-
- if (CalleeF && i < CalleeF->arg_size()) {
- if (CalleeF->getArg(i)->getType()->isPointerTy()) {
- IsByRef = true;
- }
- }
-
- if (IsByRef) {
- if (auto *Var = dynamic_cast<VariableExprAST*>(ArgExpr)) {
- Value *Ptr = getNamedValue(Var->getName());
- if (!Ptr) {
- reportError("Unknown variable %s in BYREF call", Var->getName().c_str());
- return;
- }
- Args.push_back(Ptr);
- } else {
- reportError("BYREF argument must be a variable.");
- return;
- }
- } else {
- Args.push_back(emitExpr(ArgExpr));
- }
- }
+ if (!marshalCallArgs(Call->getCallee(), Call->getArgs(), Args)) return;
FuncGen->emitCallStmt(Call, Args);
return;
}
@@ -899,7 +898,10 @@ void CodeGen::emitStmt(StmtAST *Stmt) {
Value *Val = emitExpr(Out->getExpr());
const TypeInfo *TypeInfo = getExprTypeInfo(Out->getExpr());
- if (!TypeInfo) TypeInfo = resolveType("INTEGER");
+ if (!TypeInfo) {
+ reportError("Cannot infer the type of the OUTPUT expression");
+ return;
+ }
emitOutputValue(Val, TypeInfo, true);
return;
}
diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc
index 0a22278..57454b1 100644
--- a/lib/CodeGen/FunctionGen.cc
+++ b/lib/CodeGen/FunctionGen.cc
@@ -41,7 +41,15 @@ void FunctionGen::createArgumentAllocas(Function *F, const std::vector<std::tupl
}
}
+const FuncSig *FunctionGen::getSignature(const std::string &Name) const {
+ auto It = Signatures.find(Name);
+ return It == Signatures.end() ? nullptr : &It->second;
+}
+
Function *FunctionGen::emitPrototype(PrototypeAST *Proto) {
+ FuncSig Sig;
+ Sig.ReturnTypeName = Proto->getReturnType();
+
std::vector<Type*> ArgTypes;
for (const auto &Arg : Proto->getArgs()) {
Type *T = getLLVMType(std::get<1>(Arg));
@@ -49,7 +57,9 @@ Function *FunctionGen::emitPrototype(PrototypeAST *Proto) {
T = T->getPointerTo();
}
ArgTypes.push_back(T);
+ Sig.Params.emplace_back(std::get<1>(Arg), std::get<2>(Arg));
}
+ Signatures[Proto->getName()] = std::move(Sig);
Type *RetType = getLLVMType(Proto->getReturnType());
FunctionType *FT = FunctionType::get(RetType, ArgTypes, false);
@@ -112,20 +122,12 @@ Function *FunctionGen::emitFunctionDef(FunctionDefAST *FuncAST,
static Value *GenerateCall(llvm::Module &Module,
llvm::IRBuilder<> &Builder,
- llvm::LLVMContext &Context,
const std::string &CalleeName,
const std::vector<llvm::Value*> &Args,
bool &HadError) {
Function *CalleeF = Module.getFunction(CalleeName);
if (!CalleeF) {
- std::vector<Type*> ArgTypes;
- for (auto *Val : Args) ArgTypes.push_back(Val->getType());
- FunctionType *FT = FunctionType::get(Type::getInt64Ty(Context), ArgTypes, false);
- CalleeF = Function::Create(FT, Function::ExternalLinkage, CalleeName, &Module);
- }
-
- if (CalleeF->arg_size() != Args.size()) {
- fprintf(stderr, "Error: Incorrect # arguments passed to %s\n", CalleeName.c_str());
+ fprintf(stderr, "Error: Call to undefined function %s\n", CalleeName.c_str());
HadError = true;
return nullptr;
}
@@ -138,11 +140,11 @@ static Value *GenerateCall(llvm::Module &Module,
}
llvm::Value *FunctionGen::emitCallExpr(CallExprAST *Call, const std::vector<llvm::Value*> &Args) {
- return GenerateCall(Module, Builder, Context, Call->getCallee(), Args, HadError);
+ return GenerateCall(Module, Builder, Call->getCallee(), Args, HadError);
}
void FunctionGen::emitCallStmt(CallStmtAST *Call, const std::vector<llvm::Value*> &Args) {
- GenerateCall(Module, Builder, Context, Call->getCallee(), Args, HadError);
+ GenerateCall(Module, Builder, Call->getCallee(), Args, HadError);
}
void FunctionGen::emitReturn(ReturnStmtAST *Ret, llvm::Value *RetVal) {