aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author_ov4 <ov4@ov4.org>2026-10-22 15:32:49 +0000
committer_ov4 <ov4@ov4.org>2026-10-22 15:32:49 +0000
commit713a313b06256a13ddefa5e257e3b80f8aa808ba (patch)
tree0288e9e9e44b9765cf2285fa3adaf82963612180
parent5a5a14be5e2683a42906da4b2ad19690e230c871 (diff)
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 <noreply@anthropic.com>
-rw-r--r--AGENTS/CONVENTIONS/STRINGS.md10
-rw-r--r--doc/language/cpsc-language-overview.7.md2
-rw-r--r--doc/language/function/cpsc-function-chr.7.md2
-rw-r--r--include/cps/CodeGen.h2
-rw-r--r--lib/CodeGen/CallGen.cc24
-rw-r--r--lib/CodeGen/ConstructorGen.cc17
-rw-r--r--lib/CodeGen/ControlFlowGen.cc19
-rw-r--r--lib/CodeGen/OperatorGen.cc27
-rw-r--r--lib/CodeGen/StringValueGen.cc51
-rw-r--r--lib/CodeGen/TypeCheckGen.cc7
-rw-r--r--pseudocode-lib/string.cps3
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<const ConstantStmtAST *, SymbolInfo> 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<size_t> &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<VariableExprAST *>(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<size_t> 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<AddrOfExprAST*>(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