From 713a313b06256a13ddefa5e257e3b80f8aa808ba Mon Sep 17 00:00:00 2001 From: _ov4 Date: Thu, 22 Oct 2026 15:32:49 +0000 Subject: feat(codegen): convert implicitly between CHAR and STRING A CHAR becomes a one-character STRING in &, comparisons, assignment, parameter passing and RETURN. A STRING becomes a CHAR in assignment, parameter passing and RETURN, with a runtime error when its length is not 1. CHAR with INTEGER or BOOLEAN still does not convert. Co-Authored-By: Claude Sonnet 5.5 --- AGENTS/CONVENTIONS/STRINGS.md | 10 ++++++ doc/language/cpsc-language-overview.7.md | 2 +- doc/language/function/cpsc-function-chr.7.md | 2 +- include/cps/CodeGen.h | 2 ++ lib/CodeGen/CallGen.cc | 24 +++++++++++++ lib/CodeGen/ConstructorGen.cc | 17 +++++++--- lib/CodeGen/ControlFlowGen.cc | 19 +++++++++-- lib/CodeGen/OperatorGen.cc | 27 +++++++++++---- lib/CodeGen/StringValueGen.cc | 51 ++++++++++++++++++++++++++++ lib/CodeGen/TypeCheckGen.cc | 7 ++++ pseudocode-lib/string.cps | 3 ++ 11 files changed, 147 insertions(+), 17 deletions(-) diff --git a/AGENTS/CONVENTIONS/STRINGS.md b/AGENTS/CONVENTIONS/STRINGS.md index 2908167..9bb81cf 100644 --- a/AGENTS/CONVENTIONS/STRINGS.md +++ b/AGENTS/CONVENTIONS/STRINGS.md @@ -11,6 +11,16 @@ are ordinary library definitions, with exact overload selection. Comparisons use case-sensitive byte ordering. A missing STRING class or required bridge method is a compile error. +**CHAR and STRING convert implicitly.** `emitCoercedExpr` turns a CHAR into a +one-character STRING (`emitStringFromChar`, STRING.NEW(1, ch)) and a STRING into a CHAR +(`emitCharFromString`, via the `_LENGTH()` and `C_STR()` bridge methods), so assignment, +parameter passing and RETURN need no special cases; a length other than 1 is a runtime +error. `resolveOperator` and `resolveCall` fall back to the STRING overload when an +exact match is missing and a CHAR is involved (`'a' & 'b'`, `"a" = 'a'`); two CHAR +operands still compare as CHARs, and CASE converts the CHAR side itself. A converted +value is a fresh temporary, so `emitAssignment` copies its source (CopySource) only when +the source type equals the target type. CHAR with INTEGER or BOOLEAN never converts. + **Ownership follows value copies.** STRING constructors/assignment prevent shared handles. Records and BYVAL arrays containing STRING recursively copy their string-bearing fields/elements; RETURN and temporary cleanup preserve ownership. diff --git a/doc/language/cpsc-language-overview.7.md b/doc/language/cpsc-language-overview.7.md index b1376d8..914126e 100644 --- a/doc/language/cpsc-language-overview.7.md +++ b/doc/language/cpsc-language-overview.7.md @@ -10,7 +10,7 @@ Keywords are uppercase and case-sensitive. Identifiers, including built-in funct Assign with ASCII `<-`: `Count <- Count + 1`. Compare with `=`, `<>`, `<`, `>`, `<=`, and `>=`. Standard-library STRING comparisons compare contents, case-sensitively. Use `+`, `-`, `*`, and `/` for arithmetic; built-in `/` always returns REAL. Use `&` to join strings. -User-defined types can overload operators with OVERLOAD FUNCTION OPERATOR; expressions then use the declared result type. See [cpsc-keyword-operator(7)](keyword/cpsc-keyword-operator.7.md). STRING cannot be implicitly converted to BOOLEAN. Typing is strict: arithmetic and unary minus accept only INTEGER and REAL, a comparison needs operands of the same type (or INTEGER with REAL), and BOOLEAN, CHAR and INTEGER never convert into one another implicitly (only INTEGER converts to REAL). Use ASC and CHR to move between CHAR and INTEGER. REAL never converts to INTEGER implicitly; use INT. +User-defined types can overload operators with OVERLOAD FUNCTION OPERATOR; expressions then use the declared result type. See [cpsc-keyword-operator(7)](keyword/cpsc-keyword-operator.7.md). STRING cannot be implicitly converted to BOOLEAN. Typing is strict: arithmetic and unary minus accept only INTEGER and REAL, a comparison needs operands of the same type (or INTEGER with REAL), and BOOLEAN, CHAR and INTEGER never convert into one another implicitly (only INTEGER converts to REAL). Use ASC and CHR to move between CHAR and INTEGER. CHAR and STRING do convert implicitly: a CHAR becomes a one-character STRING in `&`, in comparisons, in assignment, in argument passing and in RETURN, and a STRING becomes a CHAR in assignment, argument passing and RETURN (for example `c <- MID(s, i, 1)`); a STRING whose length is not 1 is a runtime error that TRY/CATCH can catch. REAL never converts to INTEGER implicitly; use INT. Precedence, from highest to lowest: unary operators, `* / DIV MOD`, `+ - &`, comparisons, AND, OR. Parentheses override precedence. diff --git a/doc/language/function/cpsc-function-chr.7.md b/doc/language/function/cpsc-function-chr.7.md index cc471a5..3ae8612 100644 --- a/doc/language/function/cpsc-function-chr.7.md +++ b/doc/language/function/cpsc-function-chr.7.md @@ -14,7 +14,7 @@ CHR(code : INTEGER) RETURNS STRING Provided by the standard string library, loaded and linked by default. -Convert the low eight bits of code to a one-byte string. The result is STRING, not CHAR. Use ASCII codes 1 to 127 for ordinary text; code 0 produces one NUL byte, so LENGTH(CHR(0)) is 1. +Convert the low eight bits of code to a one-byte string. The result is STRING, not CHAR, but a STRING of length 1 converts implicitly to CHAR on assignment. Use ASCII codes 1 to 127 for ordinary text; code 0 produces one NUL byte, so LENGTH(CHR(0)) is 1. # EXAMPLE diff --git a/include/cps/CodeGen.h b/include/cps/CodeGen.h index 539a6e6..9985f9b 100644 --- a/include/cps/CodeGen.h +++ b/include/cps/CodeGen.h @@ -133,6 +133,8 @@ class CodeGen { std::map StringConstants; const TypeInfo *requireStringType(); llvm::Value *emitStringFromCString(llvm::Value *Text); + llvm::Value *emitStringFromChar(llvm::Value *Ch); + llvm::Value *emitCharFromString(ExprAST *Expr, int Line); llvm::Value *emitStringCString(llvm::Value *Value); llvm::Value *emitCStringExpr(ExprAST *Expr); llvm::Value *emitFileNameOperand(ExprAST *E, const char *Keyword); diff --git a/lib/CodeGen/CallGen.cc b/lib/CodeGen/CallGen.cc index 4631332..43c0dd4 100644 --- a/lib/CodeGen/CallGen.cc +++ b/lib/CodeGen/CallGen.cc @@ -225,6 +225,30 @@ const FuncSig *CodeGen::resolveCall(const std::string &Name, } } if (Ordinary) return Ordinary; + // Failing an exact match, CHAR and STRING arguments convert into one + // another; the overload must be unique. + if (!HasTemplates && !HasExplicit) { + auto Scalar = [](const TypeInfo *T) { return T && (T->Kind == TypeKind::Char || T->isString()); }; + const FuncSig *Converted = nullptr; + for (const FuncSig *Sig : Overloads) { + if (Sig->Params.size() != Actual.size()) continue; + bool Match = true; + for (size_t I = 0; I < Actual.size() && Match; ++I) { + const ParamSig &P = Sig->Params[I]; + const TypeInfo *Formal = Types->resolve(P.TypeName); + Match = Actual[I].Rank == (P.IsArray ? P.Rank : 0) && Actual[I].Type && + (Actual[I].Type->sameType(Formal) || + (!P.IsByRef && !P.IsArray && Scalar(Actual[I].Type) && Scalar(Formal))); + } + if (!Match) continue; + if (Converted) { + reportError("Ambiguous call to %s(%s)", Name.c_str(), Key.c_str()); + return nullptr; + } + Converted = Sig; + } + if (Converted) return Converted; + } if (HasTemplates) { // A matching non-variadic template wins before any variadic candidate. for (bool Variadic : {false, true}) { diff --git a/lib/CodeGen/ConstructorGen.cc b/lib/CodeGen/ConstructorGen.cc index 286435e..e956f9d 100644 --- a/lib/CodeGen/ConstructorGen.cc +++ b/lib/CodeGen/ConstructorGen.cc @@ -22,9 +22,11 @@ Value *CodeGen::emitOwnedValue(ExprAST *Expr, const TypeInfo *Type, std::vector &Transfers) { if (!Type || (!Type->IsClass && !containsString(Type))) return emitCoercedExpr(Expr, Type); const TypeInfo *Source = getExprTypeInfo(Expr); - if (!Source || !Source->sameType(Type)) return emitCoercedExpr(Expr, Type); + // A CHAR argument becomes a fresh STRING temporary, owned like any other. + bool FromChar = Source && Type->isString() && Source->Kind == TypeKind::Char; + if (!Source || (!Source->sameType(Type) && !FromChar)) return emitCoercedExpr(Expr, Type); Value *Value = nullptr; - if (isObjectLValue(Expr)) { + if (!FromChar && isObjectLValue(Expr)) { LValueInfo LV; const auto *Var = dynamic_cast(Expr); const auto *Symbol = Var ? getSymbolInfo(Var->getName()) : nullptr; @@ -172,8 +174,11 @@ void CodeGen::emitAssignment(const LValueInfo &Target, ExprAST *Expr, const Symb } return; } - if (Info && Info->IsClass) { - auto Resolution = resolveOperator(tok_assign, Info, getExprTypeInfo(Expr)); + const TypeInfo *ExprType = getExprTypeInfo(Expr); + // A CHAR assigned to a STRING is converted first, not bound to OPERATOR<-. + bool CharToString = Info && Info->isString() && ExprType && ExprType->Kind == TypeKind::Char; + if (Info && Info->IsClass && !CharToString) { + auto Resolution = resolveOperator(tok_assign, Info, ExprType); if (Resolution.Ambiguous) { reportError("Ambiguous assignment operator"); return; @@ -190,7 +195,9 @@ void CodeGen::emitAssignment(const LValueInfo &Target, ExprAST *Expr, const Symb Value *Val = emitCoercedExpr(Expr, Info); if (!Val) return; std::vector Transfers; - bool CopySource = isObjectLValue(Expr); + // Only a value of the same type is an existing object to copy; a + // converted one is already a fresh temporary. + bool CopySource = isObjectLValue(Expr) && ExprType && ExprType->sameType(Info); if (!CopySource) { materializeOperatorReceiver(Val, Info); if (typeNeedsDestruction(Info->Name)) Transfers.push_back(StackObjects.size() - 1); diff --git a/lib/CodeGen/ControlFlowGen.cc b/lib/CodeGen/ControlFlowGen.cc index 771669d..a351162 100644 --- a/lib/CodeGen/ControlFlowGen.cc +++ b/lib/CodeGen/ControlFlowGen.cc @@ -214,9 +214,22 @@ void CodeGen::emitCaseStmt(CaseStmtAST *Stmt) { } size_t Temporaries = StackObjects.size(); Value *LabelValue = emitExpr(Label); - if (containsString(LabelType) && !isObjectLValue(Label)) + Value *LeftValue = Selected; + const TypeInfo *LeftType = SelectorType; + bool LabelConverted = false; + // CHAR and STRING compare as STRING. + if (SelectorType->isString() && LabelType->Kind == TypeKind::Char) { + LabelValue = emitStringFromChar(LabelValue); + LabelType = SelectorType; + LabelConverted = true; + } else if (SelectorType->Kind == TypeKind::Char && LabelType->isString()) { + LeftValue = emitStringFromChar(Selected); + if (LeftValue) materializeOperatorReceiver(LeftValue, LabelType); + LeftType = LabelType; + } + if (containsString(LabelType) && (LabelConverted || !isObjectLValue(Label))) materializeOperatorReceiver(LabelValue, LabelType); - auto Overload = resolveOperator(Op, SelectorType, LabelType); + auto Overload = resolveOperator(Op, LeftType, LabelType); if (Overload.Signature && !Overload.Ambiguous) { const TypeInfo *Result = resolveType(Overload.Signature->ReturnTypeName); if (!Result || Result->Kind != TypeKind::Boolean) { @@ -224,7 +237,7 @@ void CodeGen::emitCaseStmt(CaseStmtAST *Stmt) { return nullptr; } } - Value *Result = emitBinaryOp(Op, Selected, SelectorType, LabelValue, LabelType, Branch.Line); + Value *Result = emitBinaryOp(Op, LeftValue, LeftType, LabelValue, LabelType, Branch.Line); cleanupTemporaries(Temporaries); return Result; }; diff --git a/lib/CodeGen/OperatorGen.cc b/lib/CodeGen/OperatorGen.cc index 0963e05..a840408 100644 --- a/lib/CodeGen/OperatorGen.cc +++ b/lib/CodeGen/OperatorGen.cc @@ -16,13 +16,26 @@ CodeGen::OperatorResolution CodeGen::resolveOperator( auto Members = FuncGen->getOverloads(Left->Name + "." + operatorName(Op)); Candidates.insert(Candidates.end(), Members.begin(), Members.end()); } - for (const FuncSig *Sig : Candidates) { - bool Member = !Sig->Prototype->getOwnerClass().empty(); - if (Sig->Prototype->getOperator() != Op || Sig->Params.size() != (Right ? 2u : 1u) - Member) continue; - if (!Member && !Left->sameType(resolveType(Sig->Params[0].TypeName))) continue; - if (Right && !Right->sameType(resolveType(Sig->Params[Member ? 0 : 1].TypeName))) continue; - if (Result.Signature) Result.Ambiguous = true; - else Result.Signature = Sig; + // An exact match wins; failing that, a CHAR operand may stand in for a + // STRING parameter (it is converted when the arguments are marshalled). + auto Accepts = [&](const TypeInfo *Actual, const std::string &Param, bool Lenient) { + const TypeInfo *Formal = resolveType(Param); + if (Actual->sameType(Formal)) return true; + return Lenient && Actual->Kind == TypeKind::Char && Formal && Formal->isString(); + }; + // Two CHARs compare as CHARs; only '&' always builds a STRING. + bool CanConvert = Right && (Op == '&' || Left->isString() || Right->isString()); + for (bool Lenient : {false, true}) { + if (Lenient && !CanConvert) break; + for (const FuncSig *Sig : Candidates) { + bool Member = !Sig->Prototype->getOwnerClass().empty(); + if (Sig->Prototype->getOperator() != Op || Sig->Params.size() != (Right ? 2u : 1u) - Member) continue; + if (!Member && !Accepts(Left, Sig->Params[0].TypeName, Lenient)) continue; + if (Right && !Accepts(Right, Sig->Params[Member ? 0 : 1].TypeName, Lenient)) continue; + if (Result.Signature) Result.Ambiguous = true; + else Result.Signature = Sig; + } + if (Result.Signature) break; } return Result; } diff --git a/lib/CodeGen/StringValueGen.cc b/lib/CodeGen/StringValueGen.cc index c07f9a9..989bac8 100644 --- a/lib/CodeGen/StringValueGen.cc +++ b/lib/CodeGen/StringValueGen.cc @@ -35,6 +35,57 @@ Value *CodeGen::emitStringFromCString(Value *Text) { return Builder->CreateLoad(Type->LLVMType, Storage, "string"); } +// CHAR -> STRING: STRING.NEW(1, ch). +Value *CodeGen::emitStringFromChar(Value *Ch) { + const TypeInfo *Type = requireStringType(); + if (!Type || !Ch) return nullptr; + const TypeInfo *CChar = resolveType("C_CHAR"); + const FuncSig *Constructor = nullptr; + for (const FuncSig *Sig : FuncGen->getOverloads(Type->Name + ".NEW")) + if (Sig->Params.size() == 2 && !Sig->Params[1].IsArray && !Sig->Params[1].IsByRef && + CChar->sameType(resolveType(Sig->Params[1].TypeName))) + Constructor = Sig; + if (!Constructor) { + reportError("STRING requires a constructor NEW(Count : INTEGER, Ch : C_CHAR)"); + return nullptr; + } + if (!checkOperatorAccess(*Constructor)) return nullptr; + auto *Storage = CreateEntryBlockAlloca(Builder->GetInsertBlock()->getParent(), + Type->LLVMType, "string_value"); + Builder->CreateStore(Types->getZeroValue(Type->Name), Storage); + if (!emitFieldDefaults(Type, Storage, DiagnosticLine)) return nullptr; + Value *Count = ConstantInt::get(resolveType("INTEGER")->LLVMType, 1); + if (!FuncGen->emitCall(*Constructor, {Storage, Count, Ch})) return nullptr; + // The consumer takes ownership, just as with an ordinary class result. + return Builder->CreateLoad(Type->LLVMType, Storage, "string"); +} + +// STRING -> CHAR: the string must hold exactly one character. +Value *CodeGen::emitCharFromString(ExprAST *Expr, int Line) { + const TypeInfo *Type = requireStringType(); + if (!Type) return nullptr; + Value *Text = emitExpr(Expr); + if (!Text) return nullptr; + if (!isObjectLValue(Expr)) materializeOperatorReceiver(Text, Type); + auto *Receiver = CreateEntryBlockAlloca(Builder->GetInsertBlock()->getParent(), + Type->LLVMType, "string_view"); + Builder->CreateStore(Text, Receiver); + const FuncSig *Method = resolveMethod(Type->Name, "_LENGTH", {}); + if (!Method || !resolveType("INTEGER")->sameType(resolveType(Method->ReturnTypeName))) { + reportError("STRING requires a method _LENGTH() RETURNS INTEGER"); + return nullptr; + } + Value *Length = emitMethodCall(Type->Name, Receiver, "_LENGTH", {}, Line); + if (!Length) return nullptr; + Value *Wrong = Builder->CreateICmpNE(Length, ConstantInt::get(Length->getType(), 1), "char_len_bad"); + Value *Msg = Builder->CreateGlobalString("[Fatal] line %d: STRING of length %lld cannot be converted to CHAR\n", + "err_string_to_char", 0, TheModule.get()); + RuntimeChecker->emitCheck(Wrong, Msg, {RuntimeChecker->lineValue(Line), Length}); + Value *Ptr = emitMethodCall(Type->Name, Receiver, "C_STR", {}, Line); + if (!Ptr) return nullptr; + return Builder->CreateLoad(resolveType("CHAR")->LLVMType, Ptr, "char_value"); +} + Value *CodeGen::emitStringCString(Value *Value) { const TypeInfo *Type = requireStringType(); if (!Type || !Value) return nullptr; diff --git a/lib/CodeGen/TypeCheckGen.cc b/lib/CodeGen/TypeCheckGen.cc index 7cdf6ea..0f94859 100644 --- a/lib/CodeGen/TypeCheckGen.cc +++ b/lib/CodeGen/TypeCheckGen.cc @@ -30,6 +30,13 @@ Value *CodeGen::emitCoercedExpr(ExprAST *E, const TypeInfo *Target) { return nullptr; } + // CHAR and STRING convert into one another; a STRING that is not exactly + // one character long is a runtime error. + if (Src && Src->Kind == TypeKind::Char && Target->isString()) + return emitStringFromChar(emitExpr(E)); + if (Src && Src->isString() && Target->Kind == TypeKind::Char) + return emitCharFromString(E, E->getLine() ? E->getLine() : DiagnosticLine); + if (Target->isC() || (Src && Src->isC())) { if (auto *Addr = dynamic_cast(E)) { const PointerPayload *PP = Target->asPointer(); diff --git a/pseudocode-lib/string.cps b/pseudocode-lib/string.cps index 41181c4..a118326 100644 --- a/pseudocode-lib/string.cps +++ b/pseudocode-lib/string.cps @@ -52,6 +52,9 @@ CLASS STRING OVERLOAD PROCEDURE OPERATOR<-(Text : C_STRING) CALL _string_impl_assign_cstr(_HANDLE, Text) ENDPROCEDURE + FUNCTION _LENGTH() RETURNS INTEGER + RETURN (INTEGER)_string_impl_length(_HANDLE) + ENDFUNCTION FUNCTION C_STR() RETURNS C_STRING RETURN _string_impl_c_str(_HANDLE) ENDFUNCTION -- cgit v1.3.1-4-g156e