diff options
| -rw-r--r-- | AGENTS/ARCH/LAYOUT.md | 1 | ||||
| -rw-r--r-- | AGENTS/PITFALLS/core-language.md | 4 | ||||
| -rw-r--r-- | AGENTS/SUBSYSTEMS/STDLIB.md | 2 | ||||
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | doc/language/keyword/cpsc-keyword-input.7.md | 6 | ||||
| -rw-r--r-- | include/cps/CodeGen.h | 8 | ||||
| -rw-r--r-- | include/cps/TypeSystem.h | 1 | ||||
| -rw-r--r-- | lib/CodeGen/CTypeGen.cc | 17 | ||||
| -rw-r--r-- | lib/CodeGen/CodeGen.cc | 7 | ||||
| -rw-r--r-- | lib/CodeGen/InputGen.cc | 328 | ||||
| -rw-r--r-- | lib/CodeGen/NativeTypeSystem.cc | 34 | ||||
| -rw-r--r-- | lib/CodeGen/StatementGen.cc | 30 | ||||
| -rw-r--r-- | pseudocode-lib/string.cc | 9 | ||||
| -rw-r--r-- | tests/i/input-targets/input.txt | 4 | ||||
| -rw-r--r-- | tests/i/input-targets/notes.md | 4 | ||||
| -rw-r--r-- | tests/i/stdin/input.txt | 2 | ||||
| -rw-r--r-- | tests/i/stdin/notes.md | 2 | ||||
| -rw-r--r-- | tests/ii/files/program.cps | 11 | ||||
| -rw-r--r-- | tests/ii/generator-vectors.json | 2 | ||||
| -rw-r--r-- | tests/ii/strings/generate.pl | 14 | ||||
| -rw-r--r-- | tests/ii/strings/notes.md | 2 | ||||
| -rw-r--r-- | tests/ii/strings/program.cps | 2 | ||||
| -rw-r--r-- | tests/ii/strings/reference.cc | 5 |
23 files changed, 398 insertions, 98 deletions
diff --git a/AGENTS/ARCH/LAYOUT.md b/AGENTS/ARCH/LAYOUT.md index 805f1e1..dc8696e 100644 --- a/AGENTS/ARCH/LAYOUT.md +++ b/AGENTS/ARCH/LAYOUT.md @@ -12,6 +12,7 @@ | `include/cps/TemplateAST.h`, `lib/Parser/ParserTemplate.cc` | TEMPLATE declarations, type parameter packs and canonical template ids; call and pack parsing also uses ParserExpression.cc/ParserFunction.cc | | `lib/CodeGen/CodeGen.cc` | Initialization, symbols and main() compilation | | `lib/CodeGen/CallGen.cc` | Call-argument marshalling (`marshalCallArgs`, variadic arguments) and call resolution (`resolveCall`) | +| `lib/CodeGen/InputGen.cc` | INPUT token reader and validators for scalar targets, emitted as module-local IR helpers | | `lib/CodeGen/ExpressionGen.cc`, `StatementGen.cc`, `ControlFlowGen.cc` | Expression/statement dispatch and control-flow emission | | `lib/CodeGen/OperatorGen.cc`, `include/cps/Operator.h` | Exact global/member operator selection, calls and temporary receivers; shared operator spellings | | `lib/CodeGen/TemplateGen.cc`, `TemplateClone.h`, `TemplateClone.cc` | Template registry validation, cached function/class instantiation, structural type substitution, pack expansion and deferred bodies; call deduction/selection lives in CallGen.cc | diff --git a/AGENTS/PITFALLS/core-language.md b/AGENTS/PITFALLS/core-language.md index 0ddba05..dd222c7 100644 --- a/AGENTS/PITFALLS/core-language.md +++ b/AGENTS/PITFALLS/core-language.md @@ -9,7 +9,9 @@ source, most also by compiling and running test programs. mixed output lists; a final array adds no extra newline. INPUT accepts writable scalar locations, including array elements, public record/class fields and pointer dereferences, using the shared lvalue checks. Whole arrays and whole - records/classes remain unsupported INPUT targets. + records/classes remain unsupported INPUT targets. Scalar INPUT reads a validated + whitespace-delimited token (IR helpers in `InputGen.cc`, no scanf); STRING INPUT reads + the rest of the line via READ_INPUT. Bad input and end of input are runtime errors. - **Arrays cross function boundaries only as parameters.** `ArrayTable` is scoped per function, so referencing another function's array fails cleanly as "Undeclared array". Whole arrays pass as `ARRAY OF T` / `ARRAY[l:u,...] OF T` diff --git a/AGENTS/SUBSYSTEMS/STDLIB.md b/AGENTS/SUBSYSTEMS/STDLIB.md index 196ed28..d43899f 100644 --- a/AGENTS/SUBSYSTEMS/STDLIB.md +++ b/AGENTS/SUBSYSTEMS/STDLIB.md @@ -27,5 +27,5 @@ import rules: [CONVENTIONS/EXCEPTIONS_AND_UNITS.md](../CONVENTIONS/EXCEPTIONS_AN - Functions/operators use ordinary resolution; StringValueGen bridges literals, C_STR, C conversions and file operands. - C_STRING literals have an adjacent uppercase C suffix. -- INPUT delegates to READ_INPUT, OUTPUT uses C stdout buffers, and READFILE invokes +- STRING INPUT delegates to READ_INPUT (rest of the line, throws at end of input), OUTPUT uses C stdout buffers, and READFILE invokes assignment from C_STRING then frees its temporary buffer. diff --git a/CMakeLists.txt b/CMakeLists.txt index 8207da4..62dcbfc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -61,6 +61,7 @@ add_library(CPSCodeGen STATIC lib/CodeGen/ExpressionGen.cc lib/CodeGen/OperatorGen.cc lib/CodeGen/StatementGen.cc + lib/CodeGen/InputGen.cc lib/CodeGen/GlobalGen.cc lib/CodeGen/ConstantGen.cc lib/CodeGen/ControlFlowGen.cc diff --git a/doc/language/keyword/cpsc-keyword-input.7.md b/doc/language/keyword/cpsc-keyword-input.7.md index 2b4da57..9f1a137 100644 --- a/doc/language/keyword/cpsc-keyword-input.7.md +++ b/doc/language/keyword/cpsc-keyword-input.7.md @@ -12,9 +12,11 @@ INPUT target # DESCRIPTION -Read into an INTEGER, REAL, BOOLEAN, CHAR, or STRING location: a previously declared variable, an individual array element, or a public record/class field, as allowed by guide section 5.1. Nested fields, fields of array elements and pointer dereferences are supported through the existing access syntax. STRING input reads one whitespace-delimited word. +Read into an INTEGER, REAL, BOOLEAN, CHAR, or STRING location: a previously declared variable, an individual array element, or a public record/class field, as allowed by guide section 5.1. Nested fields, fields of array elements and pointer dereferences are supported through the existing access syntax. INTEGER, REAL, BOOLEAN and CHAR input read one whitespace-delimited token; after it, spaces and tabs are skipped and one following newline is consumed. STRING input reads the rest of the current line, without its line ending, so `5` and `Ada` on one line, `5 Ada`, give 5 and "Ada", and `5` then `hello` on the next line gives 5 and "hello". INTEGER accepts an optionally signed decimal that fits INTEGER, REAL accepts what STR_TO_NUM accepts, BOOLEAN accepts TRUE or FALSE in any case, and CHAR needs exactly one character. -Native C scalar targets are also supported, using their actual widths and signedness. Plain C char reads a character; signed/unsigned char read numbers. C _Bool reads a number and stores whether it is nonzero. C pointers, including C_STRING, cannot be INPUT targets. See [cpsc-keyword-c_type(7)](cpsc-keyword-c_type.7.md). +Input that does not fit the target type stops the program with a runtime error such as `[Fatal] line 3: Invalid INTEGER input 'abc'`; reading past the end of input stops it with `INPUT reached the end of input`. The error can be caught with TRY. + +Native C scalar targets are also supported, using their actual widths and signedness, and the same token rules. Plain C char reads a one-character token; signed/unsigned char and the other integers read a number that must fit the type. C _Bool reads a number and stores whether it is nonzero. C pointers, including C_STRING, cannot be INPUT targets. See [cpsc-keyword-c_type(7)](cpsc-keyword-c_type.7.md). The target address is evaluated once per INPUT execution, before reading the value. Array bounds, unset pointers and private fields are checked as for assignment. Constants, whole arrays, other whole records/classes, enums and pointer values cannot be INPUT targets. A dereferenced pointer may select a supported target type. diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 9985f9b..ffd589c 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -151,16 +151,11 @@ class CodeGen { std::unique_ptr<MemoryHandler> Memory; llvm::FunctionCallee PrintfFunc; - llvm::FunctionCallee ScanfFunc; llvm::Value *PrintfFormatStr; // %lld\n - llvm::Value *PrintfFloatFormatStr; // %f\n llvm::Value *PrintfStringFormatStr; // %s\n llvm::Value *PrintfCharFormatStr; // %c\n - llvm::Value *ScanfFormatStr; // %lld - llvm::Value *ScanfFloatFormatStr; // %lf - llvm::Value *TrueStr; // "TRUE" llvm::Value *FalseStr; // "FALSE" llvm::Value *EmptyStringStr; // "" @@ -184,7 +179,8 @@ class CodeGen { llvm::Value *emitCBinaryOp(int Op, llvm::Value *L, const TypeInfo *Left, llvm::Value *R, const TypeInfo *Right, int Line); void emitCOutput(llvm::Value *Val, const TypeInfo *Info, bool AppendNewline); - void emitCInput(llvm::Value *Addr, const TypeInfo *Info); + void emitCInput(llvm::Value *Addr, const TypeInfo *Info, int Line); + void emitScalarInput(llvm::Value *Addr, const TypeInfo *Info, int Line); // TYPE-system registration (Designator.cc) and operators (TypeCheckGen.cc). void runTypePrePass(const std::vector<std::unique_ptr<StmtAST>> &Statements, int Phase); diff --git a/include/cps/TypeSystem.h b/include/cps/TypeSystem.h index 0c7c888..91a86c3 100644 --- a/include/cps/TypeSystem.h +++ b/include/cps/TypeSystem.h @@ -77,7 +77,6 @@ struct TypeInfo { std::string CIdentity; unsigned CRank = 0; bool CSigned = false; - std::string CInputFormat; std::string COutputFormat; bool isC() const; diff --git a/lib/CodeGen/CTypeGen.cc b/lib/CodeGen/CTypeGen.cc index 18326cb..90f0aa3 100644 --- a/lib/CodeGen/CTypeGen.cc +++ b/lib/CodeGen/CTypeGen.cc @@ -186,23 +186,10 @@ void CodeGen::emitCOutput(Value *Val, const TypeInfo *Info, bool AppendNewline) Builder->CreateCall(PrintfFunc, {Fmt, Val}); } -void CodeGen::emitCInput(Value *Addr, const TypeInfo *Info) { +void CodeGen::emitCInput(Value *Addr, const TypeInfo *Info, int Line) { if (Info->Kind != TypeKind::CScalar || Info->isVoid()) { reportError("INPUT is not supported for C pointer or void type %s", Info->Name.c_str()); return; } - if (Info->isBoolean()) { - const TypeInfo *NativeInt = resolveType("C_INT"); - AllocaInst *Temp = CreateEntryBlockAlloca(Builder->GetInsertBlock()->getParent(), - NativeInt->LLVMType, "c_bool_input"); - Builder->CreateStore(Types->getZeroValue("C_INT"), Temp); - Value *Fmt = Builder->CreateGlobalString("%d", "fmt_c_input_bool", 0, TheModule.get()); - Builder->CreateCall(ScanfFunc, {Fmt, Temp}); - Value *Input = Builder->CreateLoad(NativeInt->LLVMType, Temp); - Value *Bool = convertNumericValue(Input, NativeInt, Info); - Builder->CreateStore(Bool, Addr); - return; - } - Value *Fmt = Builder->CreateGlobalString(Info->CInputFormat, "fmt_c_input", 0, TheModule.get()); - Builder->CreateCall(ScanfFunc, {Fmt, Addr}); + emitScalarInput(Addr, Info, Line); } diff --git a/lib/CodeGen/CodeGen.cc b/lib/CodeGen/CodeGen.cc index 772dc82..64ed7c4 100644 --- a/lib/CodeGen/CodeGen.cc +++ b/lib/CodeGen/CodeGen.cc @@ -94,17 +94,10 @@ void CodeGen::SetupExternalFunctions() { FunctionType *PrintfType = FunctionType::get(Type::getInt32Ty(*TheContext), PrintfArgs, true); PrintfFunc = TheModule->getOrInsertFunction("printf", PrintfType); - FunctionType *ScanfType = FunctionType::get(Type::getInt32Ty(*TheContext), PrintfArgs, true); - ScanfFunc = TheModule->getOrInsertFunction("scanf", ScanfType); - PrintfFormatStr = Builder->CreateGlobalString("%lld\n", "fmt_nl", 0, TheModule.get()); - PrintfFloatFormatStr = Builder->CreateGlobalString("%f\n", "fmt_flt", 0, TheModule.get()); PrintfStringFormatStr = Builder->CreateGlobalString("%s\n", "fmt_str", 0, TheModule.get()); PrintfCharFormatStr = Builder->CreateGlobalString("%c\n", "fmt_chr", 0, TheModule.get()); - ScanfFormatStr = Builder->CreateGlobalString("%lld", "fmt_in", 0, TheModule.get()); - ScanfFloatFormatStr = Builder->CreateGlobalString("%lf", "fmt_in_flt", 0, TheModule.get()); - TrueStr = Builder->CreateGlobalString("TRUE", "str_true", 0, TheModule.get()); FalseStr = Builder->CreateGlobalString("FALSE", "str_false", 0, TheModule.get()); EmptyStringStr = Builder->CreateGlobalString("", "str_empty", 0, TheModule.get()); diff --git a/lib/CodeGen/InputGen.cc b/lib/CodeGen/InputGen.cc new file mode 100644 index 0000000..349e7ce --- /dev/null +++ b/lib/CodeGen/InputGen.cc @@ -0,0 +1,328 @@ +#include "cps/CodeGen.h" +#include "cps/RuntimeCheck.h" +#include "llvm/IR/Intrinsics.h" + +using namespace llvm; +using namespace cps; + +namespace { +// Defines (once per module) `ptr __cps_input_token()`: reads one whitespace-delimited token from +// stdin into a malloc'd NUL-terminated buffer, or returns null at end of input. After the token it +// skips spaces, tabs and carriage returns and consumes one newline if it follows, so the next +// INPUT of a STRING starts on the following line. The caller frees the buffer. +FunctionCallee getInputTokenHelper(Module &M) { + LLVMContext &Ctx = M.getContext(); + Type *I8 = Type::getInt8Ty(Ctx), *I32 = Type::getInt32Ty(Ctx), *I64 = Type::getInt64Ty(Ctx); + PointerType *Ptr = PointerType::getUnqual(Ctx); + FunctionType *HelperTy = FunctionType::get(Ptr, {}, false); + if (Function *Existing = M.getFunction("__cps_input_token")) return {HelperTy, Existing}; + + FunctionCallee Getchar = M.getOrInsertFunction("getchar", FunctionType::get(I32, {}, false)); + FunctionCallee Ungetc = M.getOrInsertFunction("ungetc", FunctionType::get(I32, {I32, Ptr}, false)); + FunctionCallee Isspace = M.getOrInsertFunction("isspace", FunctionType::get(I32, {I32}, false)); + FunctionCallee Realloc = M.getOrInsertFunction("realloc", FunctionType::get(Ptr, {Ptr, I64}, false)); + + Function *F = Function::Create(HelperTy, Function::InternalLinkage, "__cps_input_token", &M); + BasicBlock *Entry = BasicBlock::Create(Ctx, "entry", F); + BasicBlock *SkipHead = BasicBlock::Create(Ctx, "skip.head", F); + BasicBlock *SkipTest = BasicBlock::Create(Ctx, "skip.test", F); + BasicBlock *Append = BasicBlock::Create(Ctx, "append", F); + BasicBlock *Grow = BasicBlock::Create(Ctx, "grow", F); + BasicBlock *Store = BasicBlock::Create(Ctx, "store", F); + BasicBlock *ReadTest = BasicBlock::Create(Ctx, "read.test", F); + BasicBlock *Finish = BasicBlock::Create(Ctx, "finish", F); + BasicBlock *TrailHead = BasicBlock::Create(Ctx, "trail.head", F); + BasicBlock *TrailSkip = BasicBlock::Create(Ctx, "trail.skip", F); + BasicBlock *TrailEnd = BasicBlock::Create(Ctx, "trail.end", F); + BasicBlock *PushBack = BasicBlock::Create(Ctx, "pushback", F); + BasicBlock *Done = BasicBlock::Create(Ctx, "done", F); + BasicBlock *Eof = BasicBlock::Create(Ctx, "eof", F); + + IRBuilder<> B(Entry); + Value *Buf = B.CreateAlloca(Ptr, nullptr, "buf"); + Value *Len = B.CreateAlloca(I64, nullptr, "len"); + Value *Cap = B.CreateAlloca(I64, nullptr, "cap"); + Value *Cur = B.CreateAlloca(I32, nullptr, "cur"); + B.CreateStore(ConstantPointerNull::get(Ptr), Buf); + B.CreateStore(ConstantInt::get(I64, 0), Len); + B.CreateStore(ConstantInt::get(I64, 0), Cap); + B.CreateBr(SkipHead); + + // Leading whitespace. + B.SetInsertPoint(SkipHead); + Value *First = B.CreateCall(Getchar); + B.CreateStore(First, Cur); + B.CreateCondBr(B.CreateICmpEQ(First, ConstantInt::get(I32, -1)), Eof, SkipTest); + B.SetInsertPoint(SkipTest); + Value *IsSpace = B.CreateICmpNE(B.CreateCall(Isspace, {First}), ConstantInt::get(I32, 0)); + B.CreateCondBr(IsSpace, SkipHead, Append); + + // Append Cur to the buffer, keeping room for the byte and the terminator. + B.SetInsertPoint(Append); + Value *L = B.CreateLoad(I64, Len), *C = B.CreateLoad(I64, Cap); + B.CreateCondBr(B.CreateICmpUGE(B.CreateAdd(L, ConstantInt::get(I64, 2)), C), Grow, Store); + B.SetInsertPoint(Grow); + Value *NewCap = B.CreateSelect(B.CreateICmpEQ(C, ConstantInt::get(I64, 0)), + ConstantInt::get(I64, 32), B.CreateMul(C, ConstantInt::get(I64, 2))); + B.CreateStore(B.CreateCall(Realloc, {B.CreateLoad(Ptr, Buf), NewCap}), Buf); + B.CreateStore(NewCap, Cap); + B.CreateBr(Store); + B.SetInsertPoint(Store); + Value *StoreLen = B.CreateLoad(I64, Len); + Value *Slot = B.CreateGEP(I8, B.CreateLoad(Ptr, Buf), {StoreLen}); + B.CreateStore(B.CreateTrunc(B.CreateLoad(I32, Cur), I8), Slot); + B.CreateStore(B.CreateAdd(StoreLen, ConstantInt::get(I64, 1)), Len); + Value *Next = B.CreateCall(Getchar); + B.CreateStore(Next, Cur); + B.CreateCondBr(B.CreateICmpEQ(Next, ConstantInt::get(I32, -1)), Finish, ReadTest); + B.SetInsertPoint(ReadTest); + Value *NextSpace = B.CreateICmpNE(B.CreateCall(Isspace, {Next}), ConstantInt::get(I32, 0)); + B.CreateCondBr(NextSpace, Finish, Append); + + // Terminate, then skip trailing spaces/tabs/CRs and one newline. + B.SetInsertPoint(Finish); + Value *EndLen = B.CreateLoad(I64, Len); + B.CreateStore(ConstantInt::get(I8, 0), B.CreateGEP(I8, B.CreateLoad(Ptr, Buf), {EndLen})); + B.CreateBr(TrailHead); + B.SetInsertPoint(TrailHead); + Value *Ch = B.CreateLoad(I32, Cur); + Value *Blank = B.CreateOr(B.CreateOr(B.CreateICmpEQ(Ch, ConstantInt::get(I32, ' ')), + B.CreateICmpEQ(Ch, ConstantInt::get(I32, '\t'))), + B.CreateICmpEQ(Ch, ConstantInt::get(I32, '\r'))); + B.CreateCondBr(Blank, TrailSkip, TrailEnd); + B.SetInsertPoint(TrailSkip); + B.CreateStore(B.CreateCall(Getchar), Cur); + B.CreateBr(TrailHead); + B.SetInsertPoint(TrailEnd); + Value *End = B.CreateLoad(I32, Cur); + Value *KeepsNothing = B.CreateOr(B.CreateICmpEQ(End, ConstantInt::get(I32, '\n')), + B.CreateICmpEQ(End, ConstantInt::get(I32, -1))); + B.CreateCondBr(KeepsNothing, Done, PushBack); + B.SetInsertPoint(PushBack); + // The stdin FILE* is a platform-specific symbol; host and target match. +#if defined(_WIN32) + FunctionCallee IobFunc = M.getOrInsertFunction("__acrt_iob_func", FunctionType::get(Ptr, {I32}, false)); + Value *Stdin = B.CreateCall(IobFunc, {ConstantInt::get(I32, 0)}); +#else +#if defined(__APPLE__) + const char *StdinName = "__stdinp"; +#else + const char *StdinName = "stdin"; +#endif + GlobalVariable *StdinVar = M.getGlobalVariable(StdinName); + if (!StdinVar) + StdinVar = new GlobalVariable(M, Ptr, false, GlobalValue::ExternalLinkage, nullptr, StdinName); + Value *Stdin = B.CreateLoad(Ptr, StdinVar); +#endif + B.CreateCall(Ungetc, {End, Stdin}); + B.CreateBr(Done); + B.SetInsertPoint(Done); + B.CreateRet(B.CreateLoad(Ptr, Buf)); + B.SetInsertPoint(Eof); + B.CreateRet(ConstantPointerNull::get(Ptr)); + return {HelperTy, F}; +} + +// Defines `i1 __cps_parse_int(ptr token, ptr out)`: an optionally signed decimal integer that fills +// the whole token and fits in INTEGER (what strtoll accepts in base 10, without errno). +FunctionCallee getParseIntHelper(Module &M) { + LLVMContext &Ctx = M.getContext(); + Type *I1 = Type::getInt1Ty(Ctx), *I8 = Type::getInt8Ty(Ctx), *I64 = Type::getInt64Ty(Ctx); + PointerType *Ptr = PointerType::getUnqual(Ctx); + FunctionType *HelperTy = FunctionType::get(I1, {Ptr, Ptr}, false); + if (Function *Existing = M.getFunction("__cps_parse_int")) return {HelperTy, Existing}; + + Function *F = Function::Create(HelperTy, Function::InternalLinkage, "__cps_parse_int", &M); + Value *Token = F->getArg(0), *Out = F->getArg(1); + BasicBlock *Entry = BasicBlock::Create(Ctx, "entry", F); + BasicBlock *Head = BasicBlock::Create(Ctx, "head", F), *Body = BasicBlock::Create(Ctx, "body", F); + BasicBlock *Tail = BasicBlock::Create(Ctx, "tail", F), *Negate = BasicBlock::Create(Ctx, "negate", F); + BasicBlock *Finish = BasicBlock::Create(Ctx, "finish", F), *Fail = BasicBlock::Create(Ctx, "fail", F); + + IRBuilder<> B(Entry); + Value *Acc = B.CreateAlloca(I64, nullptr, "acc"); + Value *Pos = B.CreateAlloca(Ptr, nullptr, "pos"); + Value *Sign = B.CreateLoad(I8, Token); + Value *IsNeg = B.CreateICmpEQ(Sign, ConstantInt::get(I8, '-')); + Value *HasSign = B.CreateOr(IsNeg, B.CreateICmpEQ(Sign, ConstantInt::get(I8, '+'))); + Value *Start = B.CreateGEP(I8, Token, {B.CreateZExt(HasSign, I64)}); + B.CreateStore(ConstantInt::get(I64, 0), Acc); + B.CreateStore(Start, Pos); + B.CreateBr(Head); + + // Accumulate negatively so INTEGER's minimum is representable. + B.SetInsertPoint(Head); + Value *P = B.CreateLoad(Ptr, Pos); + Value *Digit = B.CreateSub(B.CreateLoad(I8, P), ConstantInt::get(I8, '0')); + B.CreateCondBr(B.CreateICmpULT(Digit, ConstantInt::get(I8, 10)), Body, Tail); + B.SetInsertPoint(Body); + Value *Old = B.CreateLoad(I64, Acc); + Value *Mul = B.CreateBinaryIntrinsic(Intrinsic::smul_with_overflow, Old, ConstantInt::get(I64, 10)); + Value *Sub = B.CreateBinaryIntrinsic(Intrinsic::ssub_with_overflow, B.CreateExtractValue(Mul, 0), + B.CreateZExt(Digit, I64)); + Value *Overflow = B.CreateOr(B.CreateExtractValue(Mul, 1), B.CreateExtractValue(Sub, 1)); + B.CreateStore(B.CreateExtractValue(Sub, 0), Acc); + B.CreateStore(B.CreateGEP(I8, P, {ConstantInt::get(I64, 1)}), Pos); + BasicBlock *Next = BasicBlock::Create(Ctx, "next", F); + B.CreateCondBr(Overflow, Fail, Next); + B.SetInsertPoint(Next); + B.CreateBr(Head); + + B.SetInsertPoint(Tail); + Value *AtEnd = B.CreateICmpEQ(B.CreateLoad(I8, P), ConstantInt::get(I8, 0)); + Value *HasDigits = B.CreateICmpNE(P, Start); + BasicBlock *Sign2 = BasicBlock::Create(Ctx, "sign", F); + B.CreateCondBr(B.CreateAnd(AtEnd, HasDigits), Sign2, Fail); + B.SetInsertPoint(Sign2); + Value *Magnitude = B.CreateLoad(I64, Acc); + B.CreateCondBr(IsNeg, Finish, Negate); + B.SetInsertPoint(Negate); + // +x needs -acc, which overflows only for INTEGER's minimum. + BasicBlock *Positive = BasicBlock::Create(Ctx, "positive", F); + B.CreateCondBr(B.CreateICmpEQ(Magnitude, ConstantInt::get(I64, APInt::getSignedMinValue(64))), Fail, Positive); + B.SetInsertPoint(Positive); + B.CreateStore(B.CreateNeg(Magnitude), Out); + B.CreateRet(ConstantInt::getTrue(Ctx)); + B.SetInsertPoint(Finish); + B.CreateStore(Magnitude, Out); + B.CreateRet(ConstantInt::getTrue(Ctx)); + B.SetInsertPoint(Fail); + B.CreateRet(ConstantInt::getFalse(Ctx)); + return {HelperTy, F}; +} + +// Defines `i1 __cps_parse_real(ptr token, ptr out)`: the rule of parseNumber in pseudocode-lib/ +// string.cc (shared by IS_NUM and STR_TO_NUM): strtod must consume the whole token and must not +// overflow to infinity. A finite overflow is told from an "inf" literal by the missing letter i. +FunctionCallee getParseRealHelper(Module &M) { + LLVMContext &Ctx = M.getContext(); + Type *I1 = Type::getInt1Ty(Ctx), *I8 = Type::getInt8Ty(Ctx), *I32 = Type::getInt32Ty(Ctx); + Type *DoubleTy = Type::getDoubleTy(Ctx); + PointerType *Ptr = PointerType::getUnqual(Ctx); + FunctionType *HelperTy = FunctionType::get(I1, {Ptr, Ptr}, false); + if (Function *Existing = M.getFunction("__cps_parse_real")) return {HelperTy, Existing}; + + FunctionCallee Strtod = M.getOrInsertFunction("strtod", FunctionType::get(DoubleTy, {Ptr, Ptr}, false)); + FunctionCallee Strchr = M.getOrInsertFunction("strchr", FunctionType::get(Ptr, {Ptr, I32}, false)); + Function *F = Function::Create(HelperTy, Function::InternalLinkage, "__cps_parse_real", &M); + Value *Token = F->getArg(0), *Out = F->getArg(1); + IRBuilder<> B(BasicBlock::Create(Ctx, "entry", F)); + Value *EndSlot = B.CreateAlloca(Ptr, nullptr, "end"); + Value *Number = B.CreateCall(Strtod, {Token, EndSlot}); + Value *Consumed = B.CreateICmpEQ(B.CreateLoad(I8, B.CreateLoad(Ptr, EndSlot)), ConstantInt::get(I8, 0)); + Value *Progress = B.CreateICmpNE(B.CreateLoad(Ptr, EndSlot), Token); + Value *IsInf = B.CreateFCmpOEQ(B.CreateUnaryIntrinsic(Intrinsic::fabs, Number), + ConstantFP::getInfinity(DoubleTy)); + Value *NoLowerI = B.CreateIsNull(B.CreateCall(Strchr, {Token, ConstantInt::get(I32, 'i')})); + Value *NoUpperI = B.CreateIsNull(B.CreateCall(Strchr, {Token, ConstantInt::get(I32, 'I')})); + Value *Overflowed = B.CreateAnd(IsInf, B.CreateAnd(NoLowerI, NoUpperI)); + B.CreateStore(Number, Out); + B.CreateRet(B.CreateAnd(B.CreateAnd(Consumed, Progress), B.CreateNot(Overflowed))); + return {HelperTy, F}; +} + +// Defines `i32 __cps_parse_bool(ptr token)`: 1 for TRUE, 0 for FALSE in any case, -1 otherwise. +FunctionCallee getParseBoolHelper(Module &M) { + LLVMContext &Ctx = M.getContext(); + Type *I8 = Type::getInt8Ty(Ctx), *I32 = Type::getInt32Ty(Ctx), *I64 = Type::getInt64Ty(Ctx); + PointerType *Ptr = PointerType::getUnqual(Ctx); + FunctionType *HelperTy = FunctionType::get(I32, {Ptr}, false); + if (Function *Existing = M.getFunction("__cps_parse_bool")) return {HelperTy, Existing}; + + FunctionCallee Strlen = M.getOrInsertFunction("strlen", FunctionType::get(I64, {Ptr}, false)); + Function *F = Function::Create(HelperTy, Function::InternalLinkage, "__cps_parse_bool", &M); + Value *Token = F->getArg(0); + BasicBlock *Entry = BasicBlock::Create(Ctx, "entry", F); + BasicBlock *IsTrue = BasicBlock::Create(Ctx, "true", F), *IsFalse = BasicBlock::Create(Ctx, "false", F); + BasicBlock *Other = BasicBlock::Create(Ctx, "other", F), *Not4 = BasicBlock::Create(Ctx, "not4", F); + IRBuilder<> B(Entry); + Value *Length = B.CreateCall(Strlen, {Token}); + // ORing 0x20 folds ASCII upper case to lower case; no other byte folds onto a letter. + auto Matches = [&](const char *Word) { + Value *All = ConstantInt::getTrue(Ctx); + for (unsigned I = 0; Word[I]; ++I) { + Value *Byte = B.CreateOr(B.CreateLoad(I8, B.CreateGEP(I8, Token, {ConstantInt::get(I64, I)})), + ConstantInt::get(I8, 0x20)); + All = B.CreateAnd(All, B.CreateICmpEQ(Byte, ConstantInt::get(I8, Word[I]))); + } + return All; + }; + BasicBlock *Check4 = BasicBlock::Create(Ctx, "check4", F), *Check5 = BasicBlock::Create(Ctx, "check5", F); + B.CreateCondBr(B.CreateICmpEQ(Length, ConstantInt::get(I64, 4)), Check4, Not4); + B.SetInsertPoint(Check4); + B.CreateCondBr(Matches("true"), IsTrue, Other); + B.SetInsertPoint(Not4); + B.CreateCondBr(B.CreateICmpEQ(Length, ConstantInt::get(I64, 5)), Check5, Other); + B.SetInsertPoint(Check5); + B.CreateCondBr(Matches("false"), IsFalse, Other); + B.SetInsertPoint(IsTrue); + B.CreateRet(ConstantInt::get(I32, 1)); + B.SetInsertPoint(IsFalse); + B.CreateRet(ConstantInt::get(I32, 0)); + B.SetInsertPoint(Other); + B.CreateRet(ConstantInt::get(I32, -1)); + return {HelperTy, F}; +} +} // namespace + +// Reads one token from stdin and stores it, validated, at Addr. A malformed token or the end of +// input is a runtime error. C scalars follow the same token model; integers must fit the C type. +void CodeGen::emitScalarInput(Value *Addr, const TypeInfo *Info, int Line) { + Module &M = *TheModule; + Type *I32 = Type::getInt32Ty(*TheContext), *I64 = Type::getInt64Ty(*TheContext); + Type *DoubleTy = Type::getDoubleTy(*TheContext); + PointerType *Ptr = PointerType::getUnqual(*TheContext); + FunctionCallee Free = M.getOrInsertFunction("free", FunctionType::get(Type::getVoidTy(*TheContext), {Ptr}, false)); + FunctionCallee Strlen = M.getOrInsertFunction("strlen", FunctionType::get(I64, {Ptr}, false)); + + Value *Token = Builder->CreateCall(getInputTokenHelper(M), {}, "input_token"); + RuntimeChecker->emitCheck(Builder->CreateIsNull(Token), + Builder->CreateGlobalString("[Fatal] line %d: INPUT reached the end of input\n", "err_input_eof", 0, &M), + {RuntimeChecker->lineValue(Line)}); + Value *TypeName = Builder->CreateGlobalString(Info->Name, "input_type_name", 0, &M); + Value *InvalidMsg = Builder->CreateGlobalString("[Fatal] line %d: Invalid %s input '%s'\n", + "err_input_invalid", 0, &M); + auto Require = [&](Value *Valid) { + RuntimeChecker->emitCheck(Builder->CreateNot(Valid), InvalidMsg, + {RuntimeChecker->lineValue(Line), TypeName, Token}); + }; + Function *Fn = Builder->GetInsertBlock()->getParent(); + + if (Info->isChar()) { + Require(Builder->CreateICmpEQ(Builder->CreateCall(Strlen, {Token}), ConstantInt::get(I64, 1))); + Builder->CreateStore(Builder->CreateLoad(Info->LLVMType, Token), Addr); + } else if (Info->isBoolean() && !Info->isC()) { + Value *Parsed = Builder->CreateCall(getParseBoolHelper(M), {Token}); + Require(Builder->CreateICmpSGE(Parsed, ConstantInt::get(I32, 0))); + Builder->CreateStore(Builder->CreateICmpNE(Parsed, ConstantInt::get(I32, 0)), Addr); + } else if (Info->isReal()) { + AllocaInst *Slot = CreateEntryBlockAlloca(Fn, DoubleTy, "input_real"); + Require(Builder->CreateCall(getParseRealHelper(M), {Token, Slot})); + Value *Number = Builder->CreateLoad(DoubleTy, Slot); + Type *Target = Info->LLVMType; + if (Target->isFloatTy()) Number = Builder->CreateFPTrunc(Number, Target); + else if (Target != DoubleTy) Number = Builder->CreateFPExt(Number, Target); + Builder->CreateStore(Number, Addr); + } else { + AllocaInst *Slot = CreateEntryBlockAlloca(Fn, I64, "input_int"); + Require(Builder->CreateCall(getParseIntHelper(M), {Token, Slot})); + Value *Number = Builder->CreateLoad(I64, Slot); + if (Info->isBoolean()) { + Builder->CreateStore(Builder->CreateICmpNE(Number, ConstantInt::get(I64, 0)), Addr); + } else { + unsigned Bits = Info->LLVMType->getIntegerBitWidth(); + Value *Stored = Number; + if (Bits < 64) { + Type *Narrow = Info->LLVMType; + Stored = Builder->CreateTrunc(Number, Narrow); + Value *Back = Info->isC() && !Info->CSigned ? Builder->CreateZExt(Stored, I64) + : Builder->CreateSExt(Stored, I64); + Require(Builder->CreateICmpEQ(Back, Number)); + } + if (Info->isC() && !Info->CSigned) + Require(Builder->CreateICmpSGE(Number, ConstantInt::get(I64, 0))); + Builder->CreateStore(Stored, Addr); + } + } + Builder->CreateCall(Free, {Token}); +} diff --git a/lib/CodeGen/NativeTypeSystem.cc b/lib/CodeGen/NativeTypeSystem.cc index d5d1c93..c1be163 100644 --- a/lib/CodeGen/NativeTypeSystem.cc +++ b/lib/CodeGen/NativeTypeSystem.cc @@ -45,7 +45,7 @@ void TypeSystem::registerBuiltins() { // of assumed LP64/LLP64 widths. LLVM's addressable bytes are eight bits. static_assert(CHAR_BIT == 8, "LLVM requires eight-bit addressable bytes"); auto Integral = [&](auto Sample, const char *Name, unsigned Rank, - const char *InputFormat, const char *OutputFormat) { + const char *OutputFormat) { using T = decltype(Sample); TypeInfo Info; Info.Name = cTypeName(Name); @@ -54,36 +54,34 @@ void TypeSystem::registerBuiltins() { Info.LLVMType = Type::getIntNTy(Context, sizeof(T) * CHAR_BIT); Info.CRank = Rank; Info.CSigned = std::is_signed<T>::value; - Info.CInputFormat = InputFormat; Info.COutputFormat = OutputFormat; Types.emplace(Info.Name, std::move(Info)); }; - Integral(char{}, "char", 2, " %c", "%c"); - Integral(static_cast<signed char>(0), "signed char", 2, "%hhd", "%hhd"); - Integral(static_cast<unsigned char>(0), "unsigned char", 2, "%hhu", "%hhu"); - Integral(short{}, "short", 3, "%hd", "%hd"); - Integral(static_cast<unsigned short>(0), "unsigned short", 3, "%hu", "%hu"); - Integral(int{}, "int", 4, "%d", "%d"); - Integral(static_cast<unsigned int>(0), "unsigned int", 4, "%u", "%u"); - Integral(long{}, "long", 5, "%ld", "%ld"); - Integral(static_cast<unsigned long>(0), "unsigned long", 5, "%lu", "%lu"); - Integral(static_cast<long long>(0), "long long", 6, "%lld", "%lld"); - Integral(static_cast<unsigned long long>(0), "unsigned long long", 6, "%llu", "%llu"); + Integral(char{}, "char", 2, "%c"); + Integral(static_cast<signed char>(0), "signed char", 2, "%hhd"); + Integral(static_cast<unsigned char>(0), "unsigned char", 2, "%hhu"); + Integral(short{}, "short", 3, "%hd"); + Integral(static_cast<unsigned short>(0), "unsigned short", 3, "%hu"); + Integral(int{}, "int", 4, "%d"); + Integral(static_cast<unsigned int>(0), "unsigned int", 4, "%u"); + Integral(long{}, "long", 5, "%ld"); + Integral(static_cast<unsigned long>(0), "unsigned long", 5, "%lu"); + Integral(static_cast<long long>(0), "long long", 6, "%lld"); + Integral(static_cast<unsigned long long>(0), "unsigned long long", 6, "%llu"); - auto Floating = [&](auto Sample, const char *Name, unsigned Rank, const char *Format) { + auto Floating = [&](auto Sample, const char *Name, unsigned Rank) { TypeInfo Info; Info.Name = cTypeName(Name); Info.CIdentity = Name; Info.Kind = TypeKind::CScalar; Info.LLVMType = nativeFloatType<decltype(Sample)>(Context); Info.CRank = Rank; - Info.CInputFormat = Format; Info.COutputFormat = Rank == 9 ? "%Lf" : "%f"; Types.emplace(Info.Name, std::move(Info)); }; - Floating(float{}, "float", 7, "%f"); - Floating(double{}, "double", 8, "%lf"); - Floating(static_cast<long double>(0), "long double", 9, "%Lf"); + Floating(float{}, "float", 7); + Floating(double{}, "double", 8); + Floating(static_cast<long double>(0), "long double", 9); TypeInfo Bool; Bool.Name = cTypeName("_Bool"); diff --git a/lib/CodeGen/StatementGen.cc b/lib/CodeGen/StatementGen.cc index 0b14cf2..c161049 100644 --- a/lib/CodeGen/StatementGen.cc +++ b/lib/CodeGen/StatementGen.cc @@ -252,35 +252,15 @@ void CodeGen::emitInputStmt(InputStmtAST *In) { return; } - std::vector<Value*> Args; if (TypeInfo->isC()) { - emitCInput(Target.Addr, TypeInfo); - } else if (TypeInfo->isReal()) { - Args.push_back(ScanfFloatFormatStr); - Args.push_back(Target.Addr); - Builder->CreateCall(ScanfFunc, Args); - } else if (TypeInfo->isBoolean()) { - AllocaInst *TempInt = CreateEntryBlockAlloca(Builder->GetInsertBlock()->getParent(), "tmp_bool_input"); - Args.push_back(ScanfFormatStr); - Args.push_back(TempInt); - Builder->CreateCall(ScanfFunc, Args); - - Value *Val = Builder->CreateLoad(Type::getInt64Ty(*TheContext), TempInt); - Value *BoolVal = Builder->CreateICmpNE(Val, ConstantInt::get(*TheContext, APInt(64, 0)), "bool_cast"); - Builder->CreateStore(BoolVal, Target.Addr); + emitCInput(Target.Addr, TypeInfo, In->getLine()); } else if (TypeInfo->isString()) { emitMethodCall(TypeInfo->Name, Target.Addr, "READ_INPUT", {}, In->getLine()); - } else if (TypeInfo->isChar()) { - Value *CharFmt = Builder->CreateGlobalString(" %c", "fmt_in_char", 0, TheModule.get()); - Args.push_back(CharFmt); - Args.push_back(Target.Addr); - Builder->CreateCall(ScanfFunc, Args); - } else if (TypeInfo->Kind == TypeKind::Integer) { - Args.push_back(ScanfFormatStr); - Args.push_back(Target.Addr); - Builder->CreateCall(ScanfFunc, Args); + } else if (TypeInfo->Kind == TypeKind::Integer || TypeInfo->isReal() || TypeInfo->isBoolean() || + TypeInfo->isChar()) { + emitScalarInput(Target.Addr, TypeInfo, In->getLine()); } else { - // Enum/Record/Pointer and any future kind: scanf'ing raw bytes + // Enum/Record/Pointer and any future kind: reading raw bytes // into typed storage would corrupt it — reject explicitly. reportError("INPUT is not supported for a value of type %s", TypeInfo->Name.c_str()); } diff --git a/pseudocode-lib/string.cc b/pseudocode-lib/string.cc index a4c7343..c47899c 100644 --- a/pseudocode-lib/string.cc +++ b/pseudocode-lib/string.cc @@ -180,16 +180,17 @@ const char *_string_impl_c_str(void *Handle) { int _string_impl_compare(void *A, void *B) { return checked([&] { return text(A).compare(text(B)); }); } +// INPUT of a STRING: the rest of the current line, without its "\n" or "\r\n". void _string_impl_input(void *Handle) { checked([&] { std::string Value; - int Ch; - do { Ch = std::getchar(); } while (Ch != EOF && std::isspace(static_cast<unsigned char>(Ch))); - while (Ch != EOF && !std::isspace(static_cast<unsigned char>(Ch))) { + int Ch = std::getchar(); + if (Ch == EOF) throw std::runtime_error("INPUT reached the end of input"); + while (Ch != EOF && Ch != '\n') { Value.push_back(static_cast<char>(Ch)); Ch = std::getchar(); } - if (Ch != EOF) std::ungetc(Ch, stdin); + if (!Value.empty() && Value.back() == '\r') Value.pop_back(); text(Handle) = std::move(Value); }); } diff --git a/tests/i/input-targets/input.txt b/tests/i/input-targets/input.txt index dc0af77..8d0376e 100644 --- a/tests/i/input-targets/input.txt +++ b/tests/i/input-targets/input.txt @@ -1,2 +1,4 @@ 10 20 30 -40 50 Ada 1 A 2.5 -7 60 70 80 90 100 +40 50 +Ada +TRUE A 2.5 -7 60 70 80 90 100 diff --git a/tests/i/input-targets/notes.md b/tests/i/input-targets/notes.md index dded40c..57c7db4 100644 --- a/tests/i/input-targets/notes.md +++ b/tests/i/input-targets/notes.md @@ -2,6 +2,6 @@ Human review: pending. Expected output was independently traced by AI, not captured from cpsc. -Basis: CIE guide p. 15 section 5.1 allows individual data-structure elements. Native C input, numeric BOOLEAN input, whitespace-delimited STRING input and exceptions follow project behavior. +Basis: CIE guide p. 15 section 5.1 allows individual data-structure elements. Native C input, token-based scalar input, rest-of-line STRING input and exceptions follow project behavior. -Trace: the loop stores 10, 20, 30; the grid receives 40. Student 2 receives 50, Ada, true, A, 2.5 and -7, then its score becomes 80 through P. The class fields receive 60 and 70. NextIndex prints once and selects Scores[1], which receives 90. Bounds and null-pointer failures occur before reading input, leaving 100 for the final valid input. +Trace: the loop stores 10, 20, 30; the grid receives 40. Student 2 receives 50, the line Ada, true, A, 2.5 and -7, then its score becomes 80 through P. The class fields receive 60 and 70. NextIndex prints once and selects Scores[1], which receives 90. Bounds and null-pointer failures occur before reading input, leaving 100 for the final valid input. diff --git a/tests/i/stdin/input.txt b/tests/i/stdin/input.txt index 604b657..4ac051f 100644 --- a/tests/i/stdin/input.txt +++ b/tests/i/stdin/input.txt @@ -1,4 +1,4 @@ -7 -1 +TRUE Z hello diff --git a/tests/i/stdin/notes.md b/tests/i/stdin/notes.md index 62f8727..63f1a22 100644 --- a/tests/i/stdin/notes.md +++ b/tests/i/stdin/notes.md @@ -2,4 +2,4 @@ Human review: pending. Expected output was independently traced by AI, not captured from cpsc. -Basis and trace: CIE p. 15; the project INPUT contract reads BOOLEAN as an integer (nonzero is true) and STRING as a whitespace-delimited token. +Basis and trace: CIE p. 15; the project INPUT contract reads BOOLEAN as TRUE or FALSE in any case, other scalars as whitespace-delimited tokens, and STRING as the rest of the line. diff --git a/tests/ii/files/program.cps b/tests/ii/files/program.cps index f825118..a9f22c0 100644 --- a/tests/ii/files/program.cps +++ b/tests/ii/files/program.cps @@ -3,12 +3,19 @@ DECLARE Index : INTEGER DECLARE Text : STRING DECLARE Extra : STRING DECLARE Line : STRING +DECLARE Pair : STRING +DECLARE Cut : INTEGER DECLARE Combined : STRING DECLARE Lines : INTEGER INPUT Count FOR Index <- 1 TO Count - INPUT Text - INPUT Extra + INPUT Pair + Cut <- 1 + WHILE MID(Pair, Cut, 1) <> " " + Cut <- Cut + 1 + ENDWHILE + Text <- LEFT(Pair, Cut - 1) + Extra <- RIGHT(Pair, LENGTH(Pair) - Cut) OPENFILE "roundtrip.txt" FOR WRITE WRITEFILE "roundtrip.txt", Text WRITEFILE "roundtrip.txt", "" diff --git a/tests/ii/generator-vectors.json b/tests/ii/generator-vectors.json index d6a95a6..daa542d 100644 --- a/tests/ii/generator-vectors.json +++ b/tests/ii/generator-vectors.json @@ -7,5 +7,5 @@ "overload-functions": "1c2c3aedfed36812d8f7cd1648046b134eebac1fd73260558ed4d486e3d72c6e", "overload-methods": "f02d8d7e31c3c03ccf647d7e2bda18cdedd706db38308cdd35f8a3b5f87b2905", "real": "8ffb0244589eff8a1d873ca6f1e1d66cc84944ab68d8bfafa118e941891ac518", - "strings": "c8b1aa74413d7e2a673baa27e495745b5fdf799ba423e0180620b8427cd1c42a" + "strings": "2f5d7100ff29871403d339402681d8974f7955387ebddca36ec61cbf8b391a8e" } diff --git a/tests/ii/strings/generate.pl b/tests/ii/strings/generate.pl index f3ac098..57aeef0 100644 --- a/tests/ii/strings/generate.pl +++ b/tests/ii/strings/generate.pl @@ -6,18 +6,18 @@ use Random; my ($seed, $count) = @ARGV; my $rng = Random->new($seed, "strings"); my @edges = ( - '1 placeholder 1 0', - '0 A 1 1', - '0 Ab09 4 1', - '0 Ab09 1 4', - '0 Ab09 2 0', + '1 1 0 placeholder', + '0 1 1 A', + '0 4 1 Ab09', + '0 1 4 Ab09', + '0 2 0 Ab09', ); $rng->emit($count, \@edges, sub { my $size = $rng->integer(0,24); -return '1 placeholder 1 0' if $size == 0; +return '1 1 0 placeholder' if $size == 0; my @alphabet = split //, 'ABCXYZabcxyz019@_-'; my $text = join '', map { $alphabet[$rng->integer(0,$#alphabet)] } 1 .. $size; my $start = $rng->integer(1,$size); my $width = $rng->integer(0,$size-$start+1); -return "0 $text $start $width"; +return "0 $start $width $text"; }); diff --git a/tests/ii/strings/notes.md b/tests/ii/strings/notes.md index 01ea90a..468cff4 100644 --- a/tests/ii/strings/notes.md +++ b/tests/ii/strings/notes.md @@ -1,5 +1,5 @@ # strings -CIE pp. 16-17 for LENGTH/MID/RIGHT/concatenation; full-STRING LCASE/UCASE is a project extension. Inputs are ASCII under the default C locale. Empty input is encoded with a flag because project INPUT reads tokens. Substrings remain in range; zero width is a documented project boundary. +CIE pp. 16-17 for LENGTH/MID/RIGHT/concatenation; full-STRING LCASE/UCASE is a project extension. Inputs are ASCII under the default C locale. The text is the last field of each record because INPUT into a STRING reads the rest of the line; empty text is still encoded with a flag so that every record has a visible text field. Substrings remain in range; zero width is a documented project boundary. Each record produces exactly one output line. Random records are preceded by fixed boundary records; every arithmetic intermediate is bounded. diff --git a/tests/ii/strings/program.cps b/tests/ii/strings/program.cps index 55e7541..8a5e509 100644 --- a/tests/ii/strings/program.cps +++ b/tests/ii/strings/program.cps @@ -7,9 +7,9 @@ DECLARE Width : INTEGER INPUT Count FOR Index <- 1 TO Count INPUT Empty - INPUT Text INPUT Start INPUT Width + INPUT Text IF Empty = 1 THEN Text <- "" ENDIF diff --git a/tests/ii/strings/reference.cc b/tests/ii/strings/reference.cc index 54d6efc..ec67b8c 100644 --- a/tests/ii/strings/reference.cc +++ b/tests/ii/strings/reference.cc @@ -8,7 +8,10 @@ int main() { for (int i = 0; i < count; ++i) { int empty, start, width; std::string text; - if (!(std::cin >> empty >> text >> start >> width)) return 1; + if (!(std::cin >> empty >> start >> width)) return 1; + // The text is the rest of the line, as for INPUT into a STRING. + if (!std::getline(std::cin >> std::ws, text)) return 1; + if (!text.empty() && text.back() == '\r') text.pop_back(); if (empty) text.clear(); auto lower = text, upper = text; for (auto &c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c))); |
