Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions core/prelude/types/string.carbon
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ import library "prelude/copy";
import library "prelude/destroy";
import library "prelude/types/char";
import library "prelude/types/uint";
import library "prelude/operators/index";
import library "prelude/types/int";
import library "prelude/operators/as";

class String;

// Forward declaration for builtin function
fn StringAt(s: String, index: i32) -> Char;

class String {
fn Size[self: Self]() -> u64 { return self.size; }
Expand All @@ -16,8 +24,15 @@ class String {
fn Op[self: Self]() -> Self { return {.ptr = self.ptr, .size = self.size}; }
}

impl forall [T:! ImplicitAs(i32)] as IndexWith(T) where .ElementType = Char {
fn At[self: Self](subscript: T) -> Char {
return StringAt(self, subscript);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some unresolved comments still, like this one

// TODO: This should be an array iterator.
private var ptr: Char*;
// TODO: This should be a word-sized integer.
private var size: u64;
}

fn StringAt(s: String, index: i32) -> Char = "string.at";
33 changes: 33 additions & 0 deletions toolchain/check/eval.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1687,6 +1687,39 @@ static auto MakeConstantForBuiltinCall(EvalContext& eval_context,
return context.constant_values().Get(arg_ids[0]);
}

case SemIR::BuiltinFunctionKind::StringAt: {
Phase phase = Phase::Concrete;
auto str_id = GetConstantValue(eval_context, arg_ids[0], &phase);
auto index_id = GetConstantValue(eval_context, arg_ids[1], &phase);

if (phase != Phase::Concrete) {
return MakeNonConstantResult(phase);
}

auto str_struct = eval_context.insts().GetAs<SemIR::StructValue>(str_id);
auto elements = eval_context.inst_blocks().Get(str_struct.elements_id);
CARBON_CHECK(elements.size() == 2, "String struct should have 2 fields.");

auto ptr_const_id = eval_context.constant_values().Get(elements[0]);
auto string_literal = eval_context.insts().GetAs<SemIR::StringLiteral>(
eval_context.constant_values().GetInstId(ptr_const_id));
Comment on lines +1704 to +1705
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This probably will need to change when we have ways to construct a compile-time string from another compile-time string, but I think it's okay for now.


auto string_value = eval_context.sem_ir().string_literal_values().Get(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
auto string_value = eval_context.sem_ir().string_literal_values().Get(
const auto& string_value = eval_context.sem_ir().string_literal_values().Get(

Avoid copying the string

string_literal.string_literal_id);

auto index_inst = eval_context.insts().GetAs<SemIR::IntValue>(index_id);
const auto& index_val = eval_context.ints().Get(index_inst.int_id);

auto char_value =
static_cast<uint8_t>(string_value[index_val.getZExtValue()]);

auto int_id = eval_context.ints().Add(
llvm::APSInt(llvm::APInt(32, char_value), /*isUnsigned=*/false));
return MakeConstantResult(
eval_context.context(),
SemIR::IntValue{.type_id = call.type_id, .int_id = int_id}, phase);
}

case SemIR::BuiltinFunctionKind::PrintChar:
case SemIR::BuiltinFunctionKind::PrintInt:
case SemIR::BuiltinFunctionKind::ReadChar:
Expand Down
83 changes: 83 additions & 0 deletions toolchain/check/handle_index.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,68 @@ auto HandleParseNode(Context& /*context*/, Parse::IndexExprStartId /*node_id*/)
return true;
}

// Performs bounds checking for string indexing when the index is a constant.
static auto CheckStringIndexBounds(Context& context,
SemIR::InstId operand_inst_id,
SemIR::InstId index_inst_id,
const llvm::APInt& index_int) -> void {
if (index_int.isNegative()) {
CARBON_DIAGNOSTIC(ArrayIndexNegative, Error, "index `{0}` is negative.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
CARBON_DIAGNOSTIC(ArrayIndexNegative, Error, "index `{0}` is negative.",
CARBON_DIAGNOSTIC(StringIndexNegative, Error, "index `{0}` is negative.",

TypedInt);
context.emitter().Emit(
SemIR::LocId(index_inst_id), ArrayIndexNegative,
{.type = context.insts().Get(index_inst_id).type_id(),
.value = index_int});
return;
}

auto operand_const_id = context.constant_values().Get(operand_inst_id);
if (!operand_const_id.is_constant()) {
return;
}

auto operand_const_inst_id =
context.constant_values().GetInstId(operand_const_id);
auto str_struct =
context.insts().TryGetAs<SemIR::StructValue>(operand_const_inst_id);
if (!str_struct) {
return;
}

auto elements = context.inst_blocks().Get(str_struct->elements_id);
CARBON_CHECK(elements.size() == 2, "String struct should have 2 fields.");

auto ptr_const_id = context.constant_values().Get(elements[0]);
auto ptr_inst_id = context.constant_values().GetInstId(ptr_const_id);
auto string_literal =
context.insts().TryGetAs<SemIR::StringLiteral>(ptr_inst_id);
if (!string_literal) {
return;
}

auto string_value = context.sem_ir().string_literal_values().Get(
string_literal->string_literal_id);
if (index_int.getActiveBits() > 64 ||
index_int.getZExtValue() >= string_value.size()) {
CARBON_DIAGNOSTIC(StringAtIndexOutOfBounds, Error,
"string index `{0}` is past the end of the string.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about something that says the bound in it?

Suggested change
"string index `{0}` is past the end of the string.",
"string index `{0}`is out of bounds; string has length {1}.",

TypedInt);
context.emitter().Emit(
SemIR::LocId(index_inst_id), StringAtIndexOutOfBounds,
{.type = context.insts().Get(index_inst_id).type_id(),
.value = index_int});
}
}

// Checks if the given ClassType is the String class.
static auto IsStringType(Context& context, SemIR::ClassType class_type)
-> bool {
auto& class_info = context.classes().Get(class_type.class_id);
auto identifier_id = class_info.name_id.AsIdentifierId();
return identifier_id.has_value() &&
context.identifiers().Get(identifier_id) == "String";
}

// Performs an index with base expression `operand_inst_id` and
// `operand_type_id` for types that are not an array. This checks if
// the base expression implements the `IndexWith` interface; if so, uses the
Expand Down Expand Up @@ -84,6 +146,27 @@ auto HandleParseNode(Context& context, Parse::IndexExprId node_id) -> bool {
return true;
}

case CARBON_KIND(SemIR::ClassType class_type): {
if (IsStringType(context, class_type)) {
auto index_const_id = context.constant_values().Get(index_inst_id);
if (index_const_id.is_constant()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given these bounds checks are only done for constant values, I am wondering why they are happening here instead of inside eval, more closely matching ArrayIndex behaviour? I think then we don't need the IsStringType check in here at all?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bounds checks happen here because string indexing goes through the IndexWith interface, not primitive array indexing. I thought to do this to catch constant index errors earlier during semantic analysis before constant evaluation.
I noticed this during tests as well that when I was checking bounds that it only threw errors for fully constant expressions "Test"[4], but it failed to throw for variable bindings let s: str = "Test"; let c: char = s[4]; Even though both the string value and index are compile-time constants. By checking here, the implementation can check bounds for both direct literal indexing and variable-bound strings with constant indices.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fear I was not clear. I am not wondering why it's using the ArrayIndex implementation. I am wondering if the checking can be done in the implementation of StringAt in eval: https://github.com/carbon-language/carbon-lang/pull/6329/files/153ff8269fd4a57553db0ec1a000d98ebb3279a2#diff-89653437c0b9dd4925413361e1a840632f5f4dd03dc63fee83ce2d04a1586bf1R1716 we have all the information we need here to check and return an Error instead of duplicating a lot of the code here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I attempted to move the bounds checking to eval, but the StringAt builtin is never being invoked during the check phase for non-constant strings.

// --- fail_negative_index.carbon
library "[[@TEST_NAME]]";

fn TestNegativeIndex() {
  let s: str = "Test";
  //@dump-sem-ir-begin
  let c: char = s[-1];
  //@dump-sem-ir-end
}

For example, the SemIR dump from this test case
%str.as.IndexWith.impl.At.call: init %char = call %bound_method.loc6_21.2(%s.ref, %.loc6_19.2)

This call does not have the the [concrete] annotation, implying it is not being evaluated as a constant.

I suppose the issue lies in MakeConstantForCall, more specifically

 // TODO: Some builtin calls might allow some operands to be non-constant.
  if (!has_constant_operands) {
    if (builtin_kind.IsCompTimeOnly(
            eval_context.sem_ir(), eval_context.inst_blocks().Get(call.args_id),
            call.type_id)) {
      CARBON_DIAGNOSTIC(NonConstantCallToCompTimeOnlyFunction, Error,
                        "non-constant call to compile-time-only function");
      CARBON_DIAGNOSTIC(CompTimeOnlyFunctionHere, Note,
                        "compile-time-only function declared here");
      const auto& function = eval_context.functions().Get(
          std::get<SemIR::CalleeFunction>(callee).function_id);
      eval_context.emitter()
          .Build(inst_id, NonConstantCallToCompTimeOnlyFunction)
          .Note(function.latest_decl_id(), CompTimeOnlyFunctionHere)
          .Emit();
    }
    return SemIR::ConstantId::NotConstant;
  }

Since the string variable s in my test is not constant, the call returns NotConstant before ever reaching MakeConstantForBuiltinCall where the StringAt builtin is. I suppose this limitation is known based on the comment in MakeConstantForCall, however would it be worthwhile to modify MakeConstantForCall to allow StringAt (and potentially other builtins) to be invoked even when operands are not constant?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eval is constant evaluation. Each instruction has a constant value that is either constant (an instruction representing the constant result) or that is "runtime" (aka NotConstant). See ConstantId::is_constant.

If all the operands of (inputs to) the instruction are constant, then the resulting value can also be constant. That's when we execute StringAt in eval, and produce a constant value as a result. And since we have constant inputs, we can do bounds checks there.

If any of the operands of the instruction are runtime, as in the example you wrote here, then we can not evaluate to a constant value during compile (in eval). Instead, we the call instruction goes through to lower, and we generate runtime code for it.

Does that make sense?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the following test case

// --- test_string_indexing.carbon
library "[[@TEST_NAME]]";

fn TestStringIndexing() {
  let s: str = "Test";
  //@dump-sem-ir-begin
  letc: char = s[0];
  //@dump-sem-ir-end
}

The semir shows:

%s.ref: ref %str.ee0ae5.1 = name_ref s, %s
%int_0: Core.IntLiteral = int_value 0 [concrete = constants.%int_0]
...
%str.as.IndexWith.impl.At.call: init %char = call %bound_method.loc6_20.2(%.loc6_17, %int_0)

The NameRef %s.ref does not have the [concrete] annotation, and neither does the resulting Call instruction. The index
is marked [concrete], but the call is not being evaluated as constant.

Even trying a more literal case, where the string itself is concrete

// --- fail_literal_negative_index.carbon
library "[[@TEST_NAME]]";

fn TestLiteralNegativeIndex() {
  //@dump-sem-ir-begin
  let c: char = "Test"[-1];
  //@dump-sem-ir-end
}

Generates this semir

%str: %ptr.fb0 = string_literal "Test" [concrete = constants.%str.0a6]
...
%String.val: %str.ee0ae5.1 = struct_value (%str, %int_4) [concrete = constants.%String.val]
...
str.as.IndexWith.impl.At.call: init %char = call %bound_method.loc9_26.2(%String.val, %.loc9_24.2)

The call itself still is not being evaluated as constant. Is there something preventing interface method calls from being constant evaluated?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In your first example:

  let s: str = "Test";

This is a runtime value. a : denotes a runtime binding.

Is there something preventing interface method calls from being constant evaluated?

Here's another example of an interface with builtin methods:

final impl forall [N:! IntLiteral(), M:! IntLiteral()] Int(N) as EqWith(Int(M)) {
fn Equal[self: Self](other: Int(M)) -> bool = "int.eq";
fn NotEqual[self: Self](other: Int(M)) -> bool = "int.neq";
}

Here is us calling that to generate a compile-time constant value: https://carbon.godbolt.org/z/jnfTcc4ee (the semir gets truncated)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might help:

The [concrete = on the RHS of the textual semir is the constant value that was output from eval. The call instruction is going into eval, and it is not returning a concrete constant value. It looks like it's returning ConstantId::NotConstant, and you'll probably want to run things in a debugger to figure out what exactly is going on.

Copy link
Author

@ammaralassal ammaralassal Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heres what I found, note output from lldb

TEST: toolchain/check/testdata/operators/overloaded/string_indexing.carbon (lldb)  p builtin_kind
(Carbon::SemIR::BuiltinFunctionKind) $0 = {
  Carbon::Internal::EnumBase<Carbon::SemIR::BuiltinFunctionKind, Carbon::SemIR::Internal::BuiltinFunctionKindData::RawEnum, const llvm::StringLiteral *> = (value_ = None)
}

The test attempts to evaluate s[0], which would resolve to the current implementation of At within the IndexWith interface. However, when the evaluator reaches eval, we hit this check

if (builtin_kind == SemIR::BuiltinFunctionKind::None) {
      // TODO: Eventually we'll want to treat some kinds of non-builtin
      // functions as producing constants.
      return SemIR::ConstantId::NotConstant;
    }

At is not a builtin, so we get back NotConstant thereby not hitting StringAt all.

Is there a way to make the constant evaluator "see through" the At wrapper to reach the StringAt builtin?

(Apologies for all the questions on this PR I really appreciate your patience!)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, right. Note the different way that EqWith is written:

    fn At[self: Self](subscript: T) -> Char {
      return StringAt(self, subscript);
    }
  fn Equal[self: Self](other: UInt(M)) -> bool = "int.eq";

Instead of writing a function that calls a builtin internally, we should make the At function into the builtin. Sorry I overlooked that this whole time, I didn't forsee that issue.

auto index_const_inst_id =
context.constant_values().GetInstId(index_const_id);
if (auto index_val = context.insts().TryGetAs<SemIR::IntValue>(
index_const_inst_id)) {
const auto& index_int = context.ints().Get(index_val->int_id);
CheckStringIndexBounds(context, operand_inst_id, index_inst_id,
index_int);
}
}
}

auto elem_id =
PerformIndexWith(context, node_id, operand_inst_id, index_inst_id);
context.node_stack().Push(node_id, elem_id);
return true;
}

default: {
auto elem_id =
PerformIndexWith(context, node_id, operand_inst_id, index_inst_id);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
//
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
//

// AUTOUPDATE
// TIP: To test this file alone, run:
// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/operators/overloaded/string_indexing.carbon
// TIP: To dump output, run:
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/operators/overloaded/string_indexing.carbon


// --- test_string_indexing.carbon


Comment on lines +13 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
library "[[@TEST_NAME]]";

import Core library "io";
import Core library "range";

fn PrintStr(msg: str) {
for (i: i32 in Core.Range(msg.Size() as i32)) {
Core.PrintChar(msg[i]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to print things to test indexing? I think we could just make a string, and then do an index into it and wrap that in //@dump-sem-ir-begin and //@dump-sem-ir-end so that we can see what semir comes out of it?

}
}

fn Run() {
PrintStr("Hello World!\n");
}
Comment on lines +24 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no Run in tests

Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
//
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
//

// AUTOUPDATE
// TIP: To test this file alone, run:
// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/operators/overloaded/string_indexing_negative.carbon
// TIP: To dump output, run:
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/operators/overloaded/string_indexing_negative.carbon


// --- fail_negative_index.carbon
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving this test into the string_indexing.carbon file, so we only have to compile the prelude once?


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
library "[[@TEST_NAME]]";

fn TestNegativeIndex() {
let test_str: str = "Test";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let test_str: str = "Test";
let s: str = "Test";

Everything is a test in tests :) I would just go with a simple name

// CHECK:STDERR: fail_negative_index.carbon:[[@LINE+4]]:31: error: index `-1` is negative. [ArrayIndexNegative]
// CHECK:STDERR: let c: Core.Char = test_str[-1];
// CHECK:STDERR: ^~
// CHECK:STDERR:
let c: Core.Char = test_str[-1];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let c: Core.Char = test_str[-1];
test_str[-1];

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
//
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
//

// AUTOUPDATE
// TIP: To test this file alone, run:
// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/operators/overloaded/string_indexing_out_of_bounds.carbon
// TIP: To dump output, run:
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/operators/overloaded/string_indexing_out_of_bounds.carbon

// --- fail_out_of_bounds.carbon
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this into the string_indexing.carbon file?


// CHECK:STDERR: fail_out_of_bounds.carbon:[[@LINE+4]]:27: error: string index `4` is past the end of the string. [StringAtIndexOutOfBounds]
// CHECK:STDERR: var c: Core.Char = "Test"[4];
// CHECK:STDERR: ^
// CHECK:STDERR:
var c: Core.Char = "Test"[4];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var c: Core.Char = "Test"[4];
var c: char = "Test"[4];

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In these 3 tests we have 3 pretty different formulations of the test structure, it'd be nice to just have one simple structure that we repeat. I think just

fn F() {
  let s: str = "Stuff";
  //@dump-sem-ir-begin
  ... test condition here ...
  //@dump-sem-ir-end
}

Would be simple and clear?

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Part of the Carbon Language project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
// INCLUDE-FILE: toolchain/testing/testdata/min_prelude/full.carbon
// AUTOUPDATE
// TIP: To test this file alone, run:
// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/operators/overloaded/string_indexing_wrong_type.carbon
// TIP: To dump output, run:
// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/operators/overloaded/string_indexing_wrong_type.carbon

// --- fail_wrong_type.carbon
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move into string_indexing.carbon?


fn TestWrongType() {
var x: i32 = 42;
// CHECK:STDERR: fail_wrong_type.carbon:[[@LINE+4]]:22: error: cannot access member of interface `Core.IndexWith(Core.IntLiteral)` in type `i32` that does not implement that interface [MissingImplInMemberAccess]
// CHECK:STDERR: var c: Core.Char = x[0];
// CHECK:STDERR: ^~~~
// CHECK:STDERR:
var c: Core.Char = x[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var c: Core.Char = x[0];
var c: char = x[0];

}
2 changes: 2 additions & 0 deletions toolchain/diagnostics/diagnostic_kind.def
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,7 @@ CARBON_DIAGNOSTIC_KIND(AddrOnNonSelfParam)
CARBON_DIAGNOSTIC_KIND(AddrOnNonPointerType)
CARBON_DIAGNOSTIC_KIND(ArrayBoundTooLarge)
CARBON_DIAGNOSTIC_KIND(ArrayBoundNegative)
CARBON_DIAGNOSTIC_KIND(ArrayIndexNegative)
CARBON_DIAGNOSTIC_KIND(ArrayIndexOutOfBounds)
CARBON_DIAGNOSTIC_KIND(ArrayInitFromLiteralArgCountMismatch)
CARBON_DIAGNOSTIC_KIND(ArrayInitFromExprArgCountMismatch)
Expand Down Expand Up @@ -450,6 +451,7 @@ CARBON_DIAGNOSTIC_KIND(NegativeIntInUnsignedType)
CARBON_DIAGNOSTIC_KIND(NonConstantCallToCompTimeOnlyFunction)
CARBON_DIAGNOSTIC_KIND(CompTimeOnlyFunctionHere)
CARBON_DIAGNOSTIC_KIND(SelfOutsideImplicitParamList)
CARBON_DIAGNOSTIC_KIND(StringAtIndexOutOfBounds)
CARBON_DIAGNOSTIC_KIND(StringLiteralTooLong)
CARBON_DIAGNOSTIC_KIND(StringLiteralTypeIncomplete)
CARBON_DIAGNOSTIC_KIND(StringLiteralTypeUnexpected)
Expand Down
28 changes: 28 additions & 0 deletions toolchain/lower/handle_call.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,34 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id,
return;
}

case SemIR::BuiltinFunctionKind::StringAt: {
auto string_inst_id = arg_ids[0];
auto* string_arg = context.GetValue(string_inst_id);

auto string_type_id = context.GetTypeIdOfInst(string_inst_id);
auto* string_type = context.GetType(string_type_id);
auto* string_value =
context.builder().CreateLoad(string_type, string_arg, "string.load");

auto* string_ptr_field =
context.builder().CreateExtractValue(string_value, {0}, "string.ptr");

auto* index_value = context.GetValue(arg_ids[1]);

auto* char_ptr = context.builder().CreateInBoundsGEP(
llvm::Type::getInt8Ty(context.llvm_context()), string_ptr_field,
index_value, "string.char_ptr");

auto* char_i8 = context.builder().CreateLoad(
llvm::Type::getInt8Ty(context.llvm_context()), char_ptr,
"string.char");

context.SetLocal(inst_id, context.builder().CreateZExt(
char_i8, context.GetTypeOfInst(inst_id),
"string.char.zext"));
return;
}

case SemIR::BuiltinFunctionKind::TypeAnd: {
context.SetLocal(inst_id, context.GetTypeAsValue());
return;
Expand Down
21 changes: 21 additions & 0 deletions toolchain/sem_ir/builtin_function_kind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,22 @@ struct AnyType {
return true;
}
};
// Constraint that checks if a type is Core.String.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Constraint that checks if a type is Core.String.
// Constraint that checks if a type is Core.String.

struct CoreStringType {
static auto Check(const File& sem_ir, ValidateState& /*state*/,
TypeId type_id) -> bool {
auto type_inst_id = sem_ir.types().GetInstId(type_id);
auto class_type = sem_ir.insts().TryGetAs<ClassType>(type_inst_id);
if (!class_type) {
// Not a string.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Not a string.

return false;
}

const auto& class_info = sem_ir.classes().Get(class_type->class_id);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

return sem_ir.names().GetFormatted(class_info.name_id).str() == "String";
}
};

// Constraint that requires the type to be the type type.
using Type = BuiltinType<TypeType::TypeInstId>;
Expand Down Expand Up @@ -322,6 +338,11 @@ constexpr BuiltinInfo PrintInt = {
constexpr BuiltinInfo ReadChar = {"read.char",
ValidateSignature<auto()->AnySizedInt>};

// Gets a character from a string at the given index.
constexpr BuiltinInfo StringAt = {
"string.at",
ValidateSignature<auto(CoreStringType, AnySizedInt)->AnySizedInt>};

// Returns the `Core.CharLiteral` type.
constexpr BuiltinInfo CharLiteralMakeType = {"char_literal.make_type",
ValidateSignature<auto()->Type>};
Expand Down
1 change: 1 addition & 0 deletions toolchain/sem_ir/builtin_function_kind.def
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(PrimitiveCopy)
CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(PrintChar)
CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(PrintInt)
CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(ReadChar)
CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(StringAt)

// Type factories.
CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(CharLiteralMakeType)
Expand Down