aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-17 05:27:14 +0000
committer_ov4 <ov4@ov4.org>2026-10-17 05:27:14 +0000
commite782898e420c7f2abd83b97c160194d0e70598dd (patch)
treef1d95ee7b2766ac2f443df0cb6d5ba419085649e
parent42c1649264153940de143741337c38042c32e60b (diff)
feat(calls): support standalone calls and void pointer casts
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
-rw-r--r--AGENTS/CONVENTIONS.md4
-rw-r--r--doc/language/keyword/cpsc-keyword-c_type.7.md2
-rw-r--r--doc/language/keyword/cpsc-keyword-call.7.md2
-rw-r--r--doc/language/keyword/cpsc-keyword-function.7.md2
-rw-r--r--include/cps/CodeGen.h2
-rw-r--r--include/cps/FunctionGen.h4
-rw-r--r--lib/CodeGen/CTypeGen.cc3
-rw-r--r--lib/CodeGen/ExpressionGen.cc213
-rw-r--r--lib/CodeGen/FunctionGen.cc11
-rw-r--r--lib/CodeGen/NativeTypeSystem.cc1
-rw-r--r--lib/CodeGen/StatementGen.cc4
-rw-r--r--lib/Parser/Parser.cc7
-rw-r--r--tests/i/function-call-statements/case.json1
-rw-r--r--tests/i/function-call-statements/expected.txt6
-rw-r--r--tests/i/function-call-statements/notes.md5
-rw-r--r--tests/i/function-call-statements/program.cps36
16 files changed, 182 insertions, 121 deletions
diff --git a/AGENTS/CONVENTIONS.md b/AGENTS/CONVENTIONS.md
index d1454e6..bb1b836 100644
--- a/AGENTS/CONVENTIONS.md
+++ b/AGENTS/CONVENTIONS.md
@@ -131,7 +131,9 @@ The parser drops a failed statement and resyncs at the
next token that can start a statement or end a block (`syncToStatementStart`), then
continues; the failure is always diagnosed and reflected in the exit code.
-**Calls dispatch on the declared signature.** `FunctionGen::emitPrototype` records every
+**Calls share one dispatch path.** `CodeGen::emitCall` handles expression calls, CALL statements and bare `name(args)` statements; statement calls discard results. Explicit casts may bridge C void* and nominal pseudocode pointer types.
+
+**User calls dispatch on the declared signature.** `FunctionGen::emitPrototype` records every
function's pseudocode signature (`FuncSig`: return type name + per-param `ParamSig`
{type name, mode, array rank/bounds}); `CodeGen::marshalCallArgs` — the single
call-marshalling path for both call-expr and call-stmt — requires BYREF arguments to be
diff --git a/doc/language/keyword/cpsc-keyword-c_type.7.md b/doc/language/keyword/cpsc-keyword-c_type.7.md
index 5b8ebd3..3951e26 100644
--- a/doc/language/keyword/cpsc-keyword-c_type.7.md
+++ b/doc/language/keyword/cpsc-keyword-c_type.7.md
@@ -19,7 +19,7 @@ Supported base types are _Bool; char, signed char and unsigned char; signed and
C widths, char signedness and floating-point formats come from the compiler's build platform. Generated programs must use the same platform ABI; cross-target compilation and EXTERN declarations are not implemented. INTEGER remains signed 64-bit, CHAR remains an unsigned byte, and REAL remains double precision. In particular, C_CHAR need not have CHAR's signedness.
-A cast has unary precedence and converts its following operand. Declare a type alias before using its name in a cast. Numeric, character and boolean values can be explicitly converted between pseudocode and C types. STRING can be cast to char* and back, and C pointer types can be cast to other C pointer types. Pointer/integer casts and casts involving the original pseudocode enums, records or pointer types are unsupported. Casts are values, not assignable locations.
+A cast has unary precedence and converts its following operand. Declare a type alias before using its name in a cast. Numeric, character and boolean values can be explicitly converted between pseudocode and C types. STRING can be cast to char* and back, and C pointer types can be cast to other C pointer types. C void* and original pseudocode pointer types can be explicitly cast to each other. Pointer/integer casts and casts involving pseudocode enums or whole records are unsupported. Casts are values, not assignable locations.
C/pseudocode assignment, argument passing and mixed arithmetic require an explicit cast, including ordinary literals: use n <- (C_INT)1 and n + (C_INT)1. Different C scalar types use C++ numeric conversions, integer promotions and usual arithmetic conversions. BYREF requires the same type and performs no conversion. Comparisons and logical operations return BOOLEAN. The / operator always performs real division and returns REAL, including with two C integer operands; DIV and MOD accept C integers after promotion. C values used directly as IF/WHILE conditions need a cast to BOOLEAN.
diff --git a/doc/language/keyword/cpsc-keyword-call.7.md b/doc/language/keyword/cpsc-keyword-call.7.md
index 0972e5e..2d7c553 100644
--- a/doc/language/keyword/cpsc-keyword-call.7.md
+++ b/doc/language/keyword/cpsc-keyword-call.7.md
@@ -12,7 +12,7 @@ CALL name(arguments)
# DESCRIPTION
-Invoke a previously declared procedure with its parameters. Empty argument lists may be written `CALL name()` or `CALL name`. Use function calls in expressions to obtain their results.
+Invoke a previously declared procedure with its parameters. Empty argument lists may be written `CALL name()` or `CALL name`. Use function calls in expressions to obtain their results. As compiler extensions, CALL also accepts functions and discards their results, and `name(arguments)` may stand alone without CALL for either a function or a procedure. Built-ins use the same argument checks in expressions and statements.
# EXAMPLE
diff --git a/doc/language/keyword/cpsc-keyword-function.7.md b/doc/language/keyword/cpsc-keyword-function.7.md
index a177afa..52efa90 100644
--- a/doc/language/keyword/cpsc-keyword-function.7.md
+++ b/doc/language/keyword/cpsc-keyword-function.7.md
@@ -17,6 +17,8 @@ ENDFUNCTION
Call a function in an expression as `name(arguments)`. A call may precede its top-level definition. RETURNS specifies the result type; omitting it defaults to INTEGER. RETURN supplies a value of that type. Whole-array results are unsupported.
+As a compiler extension, `name(arguments)` may also be a complete statement that discards the result. This differs from guide section 8.2, which requires function calls to occur within expressions. Built-in functions support the same form.
+
Parameters use `name : type`, optionally prefixed by BYVAL. BYVAL is the default; BYREF is forbidden for functions (guide section 8.3). Array parameters accept `ARRAY OF type` (one dimension) or `ARRAY[lower:upper, ...] OF type` (integer literal bounds). BYVAL arrays are copied. RETURNS, RETURN, BYVAL, and ENDFUNCTION belong here.
# EXAMPLE
diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h
index 883cf71..8f84004 100644
--- a/include/cps/CodeGen.h
+++ b/include/cps/CodeGen.h
@@ -106,6 +106,8 @@ class CodeGen {
bool emitLValue(ExprAST *E, LValueInfo &Out);
// The rvalue read: load + the STRING null->"" guard.
llvm::Value *loadFromLValue(const LValueInfo &LV);
+ llvm::Value *emitCall(const std::string &Callee,
+ const std::vector<std::unique_ptr<ExprAST>> &ArgExprs, int Line);
bool marshalCallArgs(const std::string &Callee,
const std::vector<std::unique_ptr<ExprAST>> &ArgExprs,
int Line,
diff --git a/include/cps/FunctionGen.h b/include/cps/FunctionGen.h
index 4bf4519..fd874d2 100644
--- a/include/cps/FunctionGen.h
+++ b/include/cps/FunctionGen.h
@@ -87,9 +87,7 @@ public:
llvm::Function *emitFunctionDef(FunctionDefAST *FuncAST, const std::function<void(StmtAST*)> &StmtEmitter, unsigned ScopeID);
- llvm::Value *emitCallExpr(CallExprAST *Call, const std::vector<llvm::Value*> &Args);
-
- void emitCallStmt(CallStmtAST *Call, const std::vector<llvm::Value*> &Args);
+ llvm::Value *emitCall(const std::string &Callee, const std::vector<llvm::Value*> &Args);
void emitReturn(ReturnStmtAST *Ret, llvm::Value *RetVal);
};
diff --git a/lib/CodeGen/CTypeGen.cc b/lib/CodeGen/CTypeGen.cc
index 7623103..b30814e 100644
--- a/lib/CodeGen/CTypeGen.cc
+++ b/lib/CodeGen/CTypeGen.cc
@@ -62,6 +62,9 @@ Value *CodeGen::emitCast(CastExprAST *Cast) {
return nullptr;
}
const TypeInfo *Source = getExprTypeInfo(Operand);
+ if (Source && ((Source->Kind == TypeKind::Pointer && Target->CIdentity == "void*") ||
+ (Source->CIdentity == "void*" && Target->Kind == TypeKind::Pointer)))
+ return emitExpr(Operand);
if (!Source || Source->isVoid() || Source->isUserKind() || Target->isUserKind()) {
reportError("Unsupported operand or target for cast to %s", Target->Name.c_str());
return nullptr;
diff --git a/lib/CodeGen/ExpressionGen.cc b/lib/CodeGen/ExpressionGen.cc
index b3655fd..fa2581f 100644
--- a/lib/CodeGen/ExpressionGen.cc
+++ b/lib/CodeGen/ExpressionGen.cc
@@ -124,110 +124,8 @@ Value *CodeGen::emitExpr(ExprAST *Expr) {
return emitBinaryOp(Bin->getOp(), L, LT, R, RT, Bin->getLine());
}
- if (auto *Call = dynamic_cast<CallExprAST*>(Expr)) {
- std::string Name = Call->getCallee();
- if (const BuiltinInfo *B = findBuiltin(Name)) {
- Name = B->Name;
- if (Call->getArgs().size() != B->Arity) {
- reportError("%s expects %u arg%s", B->Name, B->Arity, B->Arity == 1 ? "" : "s");
- return nullptr;
- }
- }
- if (Name == "LENGTH") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- if (!Str) return nullptr;
- return StrHandler->emitLength(Str);
- }
- if (Name == "MID") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- Value *Start = emitCoercedExpr(Call->getArgs()[1].get(), resolveType("INTEGER"));
- Value *Len = emitCoercedExpr(Call->getArgs()[2].get(), resolveType("INTEGER"));
- if (!Str || !Start || !Len) return nullptr;
- return StrHandler->emitMid(Str, Start, Len);
- }
- if (Name == "RIGHT") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- Value *Len = emitCoercedExpr(Call->getArgs()[1].get(), resolveType("INTEGER"));
- if (!Str || !Len) return nullptr;
- return StrHandler->emitRight(Str, Len);
- }
- if (Name == "LEFT") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- Value *Len = emitCoercedExpr(Call->getArgs()[1].get(), resolveType("INTEGER"));
- if (!Str || !Len) return nullptr;
- return StrHandler->emitLeft(Str, Len);
- }
- if (Name == "LCASE") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- if (!Str) return nullptr;
- return StrHandler->emitLCase(Str);
- }
- if (Name == "UCASE") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- if (!Str) return nullptr;
- return StrHandler->emitUCase(Str);
- }
- if (Name == "ASC") {
- const TypeInfo *ArgType = getExprTypeInfo(Call->getArgs()[0].get());
- if (ArgType && (ArgType->isUserKind() || ArgType->isC())) {
- reportError("ASC expects a CHAR or STRING (got %s)", ArgType->Name.c_str());
- return nullptr;
- }
- Value *ArgVal = emitExpr(Call->getArgs()[0].get());
- if (!ArgVal) return nullptr;
- Value *CharVal = nullptr;
- if (ArgType && ArgType->isChar()) {
- CharVal = coerceValueToType(ArgVal, resolveType("CHAR"));
- } else {
- CharVal = Builder->CreateLoad(Type::getInt8Ty(*TheContext), ArgVal, "char_load");
- }
- Value *AscVal = Builder->CreateZExt(CharVal, Type::getInt32Ty(*TheContext), "asc_val");
- return coerceValueToType(AscVal, resolveType("INTEGER"));
- }
- if (Name == "CHR") {
- Value *IntVal = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("INTEGER"));
- if (!IntVal) return nullptr;
- Value *CharVal = Builder->CreateTrunc(IntVal, Type::getInt8Ty(*TheContext), "chr_val");
- return coerceValueToType(CharVal, resolveType("STRING"));
- }
- if (Name == "IS_NUM") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- if (!Str) return nullptr;
- return StrConvHandler->emitIsNum(Str);
- }
- if (Name == "NUM_TO_STR") {
- const TypeInfo *ArgType = getExprTypeInfo(Call->getArgs()[0].get());
- if (ArgType && (ArgType->isUserKind() || ArgType->isC())) {
- reportError("NUM_TO_STR expects a number (got %s)", ArgType->Name.c_str());
- return nullptr;
- }
- Value *NumV = emitExpr(Call->getArgs()[0].get());
- if (!NumV) return nullptr;
- bool IsReal = NumV->getType()->isDoubleTy();
- if (NumV->getType()->isIntegerTy(8)) {
- NumV = coerceValueToType(NumV, resolveType("INTEGER"));
- }
- return StrConvHandler->emitNumToStr(NumV, IsReal);
- }
- if (Name == "STR_TO_NUM") {
- Value *Str = emitCoercedExpr(Call->getArgs()[0].get(), resolveType("STRING"));
- 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 = emitCoercedExpr(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(), Call->getLine(), Args)) return nullptr;
- return FuncGen->emitCallExpr(Call, Args);
- }
+ if (auto *Call = dynamic_cast<CallExprAST*>(Expr))
+ return emitCall(Call->getCallee(), Call->getArgs(), Call->getLine());
if (auto *D = dynamic_cast<DesignatorExprAST*>(Expr)) {
LValueInfo LV;
@@ -274,3 +172,110 @@ Value *CodeGen::emitBinaryOp(int Op, Value *L, const TypeInfo *LT,
}
return ArithHandler->emitBinaryOp(Op, L, R, Line);
}
+
+Value *CodeGen::emitCall(const std::string &Callee,
+ const std::vector<std::unique_ptr<ExprAST>> &ArgExprs, int Line) {
+ SaveAndRestore<int> Location(DiagnosticLine, Line ? Line : DiagnosticLine);
+ std::string Name = Callee;
+ if (const BuiltinInfo *B = findBuiltin(Name)) {
+ Name = B->Name;
+ if (ArgExprs.size() != B->Arity) {
+ reportError("%s expects %u arg%s", B->Name, B->Arity, B->Arity == 1 ? "" : "s");
+ return nullptr;
+ }
+ }
+ if (Name == "LENGTH") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ if (!Str) return nullptr;
+ return StrHandler->emitLength(Str);
+ }
+ if (Name == "MID") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ Value *Start = emitCoercedExpr(ArgExprs[1].get(), resolveType("INTEGER"));
+ Value *Len = emitCoercedExpr(ArgExprs[2].get(), resolveType("INTEGER"));
+ if (!Str || !Start || !Len) return nullptr;
+ return StrHandler->emitMid(Str, Start, Len);
+ }
+ if (Name == "RIGHT") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ Value *Len = emitCoercedExpr(ArgExprs[1].get(), resolveType("INTEGER"));
+ if (!Str || !Len) return nullptr;
+ return StrHandler->emitRight(Str, Len);
+ }
+ if (Name == "LEFT") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ Value *Len = emitCoercedExpr(ArgExprs[1].get(), resolveType("INTEGER"));
+ if (!Str || !Len) return nullptr;
+ return StrHandler->emitLeft(Str, Len);
+ }
+ if (Name == "LCASE") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ if (!Str) return nullptr;
+ return StrHandler->emitLCase(Str);
+ }
+ if (Name == "UCASE") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ if (!Str) return nullptr;
+ return StrHandler->emitUCase(Str);
+ }
+ if (Name == "ASC") {
+ const TypeInfo *ArgType = getExprTypeInfo(ArgExprs[0].get());
+ if (ArgType && (ArgType->isUserKind() || ArgType->isC())) {
+ reportError("ASC expects a CHAR or STRING (got %s)", ArgType->Name.c_str());
+ return nullptr;
+ }
+ Value *ArgVal = emitExpr(ArgExprs[0].get());
+ if (!ArgVal) return nullptr;
+ Value *CharVal = nullptr;
+ if (ArgType && ArgType->isChar()) {
+ CharVal = coerceValueToType(ArgVal, resolveType("CHAR"));
+ } else {
+ CharVal = Builder->CreateLoad(Type::getInt8Ty(*TheContext), ArgVal, "char_load");
+ }
+ Value *AscVal = Builder->CreateZExt(CharVal, Type::getInt32Ty(*TheContext), "asc_val");
+ return coerceValueToType(AscVal, resolveType("INTEGER"));
+ }
+ if (Name == "CHR") {
+ Value *IntVal = emitCoercedExpr(ArgExprs[0].get(), resolveType("INTEGER"));
+ if (!IntVal) return nullptr;
+ Value *CharVal = Builder->CreateTrunc(IntVal, Type::getInt8Ty(*TheContext), "chr_val");
+ return coerceValueToType(CharVal, resolveType("STRING"));
+ }
+ if (Name == "IS_NUM") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ if (!Str) return nullptr;
+ return StrConvHandler->emitIsNum(Str);
+ }
+ if (Name == "NUM_TO_STR") {
+ const TypeInfo *ArgType = getExprTypeInfo(ArgExprs[0].get());
+ if (ArgType && (ArgType->isUserKind() || ArgType->isC())) {
+ reportError("NUM_TO_STR expects a number (got %s)", ArgType->Name.c_str());
+ return nullptr;
+ }
+ Value *NumV = emitExpr(ArgExprs[0].get());
+ if (!NumV) return nullptr;
+ bool IsReal = NumV->getType()->isDoubleTy();
+ if (NumV->getType()->isIntegerTy(8)) {
+ NumV = coerceValueToType(NumV, resolveType("INTEGER"));
+ }
+ return StrConvHandler->emitNumToStr(NumV, IsReal);
+ }
+ if (Name == "STR_TO_NUM") {
+ Value *Str = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ if (!Str) return nullptr;
+ return StrConvHandler->emitStrToNum(Str, true);
+ }
+ if (Name == "EOF") {
+ if (isWholeArrayVar(ArgExprs[0].get())) {
+ reportError("Cannot use a whole array as the EOF filename");
+ return nullptr;
+ }
+ Value *FileName = emitCoercedExpr(ArgExprs[0].get(), resolveType("STRING"));
+ if (!FileName) return nullptr;
+ return Files->emitEof(FileName, Line);
+ }
+
+ std::vector<Value*> Args;
+ if (!marshalCallArgs(Name, ArgExprs, Line, Args)) return nullptr;
+ return FuncGen->emitCall(Name, Args);
+}
diff --git a/lib/CodeGen/FunctionGen.cc b/lib/CodeGen/FunctionGen.cc
index a155f60..10bd3fa 100644
--- a/lib/CodeGen/FunctionGen.cc
+++ b/lib/CodeGen/FunctionGen.cc
@@ -243,14 +243,9 @@ static Value *GenerateCall(llvm::Module &Module,
return Exceptions.emitCall(CalleeF, Args, "calltmp");
}
-llvm::Value *FunctionGen::emitCallExpr(CallExprAST *Call, const std::vector<llvm::Value*> &Args) {
- const FuncSig *Sig = getSignature(Call->getCallee());
- return GenerateCall(Module, Exceptions, Sig ? Sig->Name : Call->getCallee(), Args, HadError);
-}
-
-void FunctionGen::emitCallStmt(CallStmtAST *Call, const std::vector<llvm::Value*> &Args) {
- const FuncSig *Sig = getSignature(Call->getCallee());
- GenerateCall(Module, Exceptions, Sig ? Sig->Name : Call->getCallee(), Args, HadError);
+llvm::Value *FunctionGen::emitCall(const std::string &Callee, const std::vector<llvm::Value*> &Args) {
+ const FuncSig *Sig = getSignature(Callee);
+ return GenerateCall(Module, Exceptions, Sig ? Sig->Name : Callee, Args, HadError);
}
void FunctionGen::emitReturn(ReturnStmtAST *Ret, llvm::Value *RetVal) {
diff --git a/lib/CodeGen/NativeTypeSystem.cc b/lib/CodeGen/NativeTypeSystem.cc
index a4fcee0..2dbbc49 100644
--- a/lib/CodeGen/NativeTypeSystem.cc
+++ b/lib/CodeGen/NativeTypeSystem.cc
@@ -106,6 +106,7 @@ void TypeSystem::registerBuiltins() {
defineCAlias("C_CHAR", "char", Err);
defineCAlias("C_STRING", "char*", Err);
defineCAlias("C_DOUBLE", "double", Err);
+ importCType("void*", Err);
}
diff --git a/lib/CodeGen/StatementGen.cc b/lib/CodeGen/StatementGen.cc
index 2c80437..b648ec6 100644
--- a/lib/CodeGen/StatementGen.cc
+++ b/lib/CodeGen/StatementGen.cc
@@ -55,9 +55,7 @@ void CodeGen::emitStmt(StmtAST *Stmt) {
}
if (auto *Call = dynamic_cast<CallStmtAST*>(Stmt)) {
- std::vector<Value*> Args;
- if (!marshalCallArgs(Call->getCallee(), Call->getArgs(), Call->getLine(), Args)) return;
- FuncGen->emitCallStmt(Call, Args);
+ emitCall(Call->getCallee(), Call->getArgs(), Call->getLine());
return;
}
diff --git a/lib/Parser/Parser.cc b/lib/Parser/Parser.cc
index d4d04d2..36b0a6a 100644
--- a/lib/Parser/Parser.cc
+++ b/lib/Parser/Parser.cc
@@ -221,6 +221,13 @@ std::unique_ptr<StmtAST> Parser::ParseAssignStmt() {
int Line = Lex.getLine();
getNextToken();
+ if (CurTok == '(') {
+ getNextToken();
+ auto Args = ParseExprList(')', true);
+ if (!Args) return nullptr;
+ return std::make_unique<CallStmtAST>(Name, std::move(*Args), Line);
+ }
+
std::vector<DesignatorAccess> Accesses;
if (!ParseDesignatorSuffix(Accesses)) return nullptr;
diff --git a/tests/i/function-call-statements/case.json b/tests/i/function-call-statements/case.json
new file mode 100644
index 0000000..0967ef4
--- /dev/null
+++ b/tests/i/function-call-statements/case.json
@@ -0,0 +1 @@
+{}
diff --git a/tests/i/function-call-statements/expected.txt b/tests/i/function-call-statements/expected.txt
new file mode 100644
index 0000000..d2576ac
--- /dev/null
+++ b/tests/i/function-call-statements/expected.txt
@@ -0,0 +1,6 @@
+17
+23
+7
+19
+[Fatal] line 33: Division by zero
+caught
diff --git a/tests/i/function-call-statements/notes.md b/tests/i/function-call-statements/notes.md
new file mode 100644
index 0000000..c7e0c0a
--- /dev/null
+++ b/tests/i/function-call-statements/notes.md
@@ -0,0 +1,5 @@
+# Trace notes
+
+Human review: pending.
+
+This case exercises compiler extensions. Pointer casts preserve the addresses of INTEGER 17 and C int 23. Echo prints 7 even when its result is discarded. Discarded LENGTH calls print nothing. Two BYREF procedure calls increment 17 to 19. The argument division on line 33 throws before Echo runs; CATCH prints caught.
diff --git a/tests/i/function-call-statements/program.cps b/tests/i/function-call-statements/program.cps
new file mode 100644
index 0000000..403414b
--- /dev/null
+++ b/tests/i/function-call-statements/program.cps
@@ -0,0 +1,36 @@
+TYPE Raw = C_TYPE("void*")
+TYPE NativePointer = C_TYPE("int*")
+TYPE IntPointer = ^INTEGER
+DECLARE Number : INTEGER
+DECLARE Pointer : IntPointer
+DECLARE Address : Raw
+DECLARE Native : C_INT
+DECLARE NativeAddress : NativePointer
+Number <- 17
+Pointer <- ^Number
+Address <- (Raw)Pointer
+Pointer <- (IntPointer)Address
+OUTPUT Pointer^
+Native <- (C_INT)23
+NativeAddress <- ^Native
+Address <- (Raw)NativeAddress
+NativeAddress <- (NativePointer)Address
+OUTPUT NativeAddress^
+FUNCTION Echo(Value : INTEGER) RETURNS INTEGER
+ OUTPUT Value
+ RETURN Value
+ENDFUNCTION
+Echo(7)
+LENGTH("discard")
+CALL LENGTH("also discard")
+PROCEDURE Change(BYREF Value : INTEGER)
+ Value <- Value + 1
+ENDPROCEDURE
+Change(Number)
+CALL Change(Number)
+OUTPUT Number
+TRY
+ Echo(1 DIV 0)
+CATCH
+ OUTPUT "caught"
+ENDTRY