Commit a5dc3f0342

William Sengir <william@sengir.com>
2022-03-17 17:12:24
stage2: add safety checks for index out of bounds
1 parent 9f25c81
Changed files (1)
src/Sema.zig
@@ -3235,7 +3235,7 @@ fn zirValidateArrayInit(
         // any ZIR instructions at comptime; we need to do that here.
         if (array_ty.sentinel()) |sentinel_val| {
             const array_len_ref = try sema.addIntUnsigned(Type.usize, array_len);
-            const sentinel_ptr = try sema.elemPtrArray(block, init_src, array_ptr, array_len_ref, init_src);
+            const sentinel_ptr = try sema.elemPtrArray(block, init_src, array_ptr, init_src, array_len_ref);
             const sentinel = try sema.addConstant(array_ty.childType(), sentinel_val);
             try sema.storePtr2(block, init_src, sentinel_ptr, init_src, sentinel, init_src, .store);
         }
@@ -15746,6 +15746,7 @@ pub const PanicId = enum {
     cast_to_null,
     incorrect_alignment,
     invalid_error_code,
+    index_out_of_bounds,
 };
 
 fn addSafetyCheck(
@@ -15867,6 +15868,7 @@ fn safetyPanic(
         .cast_to_null => "cast causes pointer to be null",
         .incorrect_alignment => "incorrect alignment",
         .invalid_error_code => "invalid error code",
+        .index_out_of_bounds => "attempt to index out of bounds",
     };
 
     const msg_inst = msg_inst: {
@@ -16483,10 +16485,10 @@ fn structFieldPtr(
             return sema.analyzeRef(block, src, len_inst);
         }
         const field_index = try sema.tupleFieldIndex(block, struct_ty, field_name, field_name_src);
-        return sema.tupleFieldPtr(block, struct_ptr, field_index, src, field_name_src);
+        return sema.tupleFieldPtr(block, src, struct_ptr, field_name_src, field_index);
     } else if (struct_ty.isAnonStruct()) {
         const field_index = try sema.anonStructFieldIndex(block, struct_ty, field_name, field_name_src);
-        return sema.tupleFieldPtr(block, struct_ptr, field_index, src, field_name_src);
+        return sema.tupleFieldPtr(block, src, struct_ptr, field_name_src, field_index);
     }
 
     const struct_obj = struct_ty.castTag(.@"struct").?.data;
@@ -16806,67 +16808,55 @@ fn elemPtr(
     sema: *Sema,
     block: *Block,
     src: LazySrcLoc,
-    array_ptr: Air.Inst.Ref,
+    indexable_ptr: Air.Inst.Ref,
     elem_index: Air.Inst.Ref,
     elem_index_src: LazySrcLoc,
 ) CompileError!Air.Inst.Ref {
-    const array_ptr_src = src; // TODO better source location
-    const array_ptr_ty = sema.typeOf(array_ptr);
-    const array_ty = switch (array_ptr_ty.zigTypeTag()) {
-        .Pointer => array_ptr_ty.elemType(),
-        else => return sema.fail(block, array_ptr_src, "expected pointer, found '{}'", .{array_ptr_ty}),
+    const indexable_ptr_src = src; // TODO better source location
+    const indexable_ptr_ty = sema.typeOf(indexable_ptr);
+    const indexable_ty = switch (indexable_ptr_ty.zigTypeTag()) {
+        .Pointer => indexable_ptr_ty.elemType(),
+        else => return sema.fail(block, indexable_ptr_src, "expected pointer, found '{}'", .{indexable_ptr_ty}),
     };
-    if (!array_ty.isIndexable()) {
-        return sema.fail(block, src, "array access of non-indexable type '{}'", .{array_ty});
+    if (!indexable_ty.isIndexable()) {
+        return sema.fail(block, src, "element access of non-indexable type '{}'", .{indexable_ty});
     }
 
-    switch (array_ty.zigTypeTag()) {
+    switch (indexable_ty.zigTypeTag()) {
         .Pointer => {
-            // In all below cases, we have to deref the ptr operand to get the actual array pointer.
-            const array = try sema.analyzeLoad(block, array_ptr_src, array_ptr, array_ptr_src);
+            // In all below cases, we have to deref the ptr operand to get the actual indexable pointer.
+            const indexable = try sema.analyzeLoad(block, indexable_ptr_src, indexable_ptr, indexable_ptr_src);
             const target = sema.mod.getTarget();
-            const result_ty = try array_ty.elemPtrType(sema.arena, target);
-            switch (array_ty.ptrSize()) {
-                .Slice => {
-                    const maybe_slice_val = try sema.resolveDefinedValue(block, array_ptr_src, array);
-                    const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
-                    const runtime_src = if (maybe_slice_val) |slice_val| rs: {
-                        const index_val = maybe_index_val orelse break :rs elem_index_src;
-                        const index = @intCast(usize, index_val.toUnsignedInt());
-                        const elem_ptr = try slice_val.elemPtr(array_ty, sema.arena, index);
-                        return sema.addConstant(result_ty, elem_ptr);
-                    } else array_ptr_src;
-
-                    try sema.requireRuntimeBlock(block, runtime_src);
-                    return block.addSliceElemPtr(array, elem_index, result_ty);
-                },
+            const result_ty = try indexable_ty.elemPtrType(sema.arena, target);
+            switch (indexable_ty.ptrSize()) {
+                .Slice => return sema.elemPtrSlice(block, indexable_ptr_src, indexable, elem_index_src, elem_index),
                 .Many, .C => {
-                    const maybe_ptr_val = try sema.resolveDefinedValue(block, array_ptr_src, array);
+                    const maybe_ptr_val = try sema.resolveDefinedValue(block, indexable_ptr_src, indexable);
                     const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
 
                     const runtime_src = rs: {
-                        const ptr_val = maybe_ptr_val orelse break :rs array_ptr_src;
+                        const ptr_val = maybe_ptr_val orelse break :rs indexable_ptr_src;
                         const index_val = maybe_index_val orelse break :rs elem_index_src;
                         const index = @intCast(usize, index_val.toUnsignedInt());
-                        const elem_ptr = try ptr_val.elemPtr(array_ty, sema.arena, index);
+                        const elem_ptr = try ptr_val.elemPtr(indexable_ty, sema.arena, index);
                         return sema.addConstant(result_ty, elem_ptr);
                     };
 
                     try sema.requireRuntimeBlock(block, runtime_src);
-                    return block.addPtrElemPtr(array, elem_index, result_ty);
+                    return block.addPtrElemPtr(indexable, elem_index, result_ty);
                 },
                 .One => {
-                    assert(array_ty.childType().zigTypeTag() == .Array); // Guaranteed by isIndexable
-                    return sema.elemPtrArray(block, array_ptr_src, array, elem_index, elem_index_src);
+                    assert(indexable_ty.childType().zigTypeTag() == .Array); // Guaranteed by isIndexable
+                    return sema.elemPtrArray(block, indexable_ptr_src, indexable, elem_index_src, elem_index);
                 },
             }
         },
-        .Array, .Vector => return sema.elemPtrArray(block, array_ptr_src, array_ptr, elem_index, elem_index_src),
+        .Array, .Vector => return sema.elemPtrArray(block, indexable_ptr_src, indexable_ptr, elem_index_src, elem_index),
         .Struct => {
             // Tuple field access.
             const index_val = try sema.resolveConstValue(block, elem_index_src, elem_index);
             const index = @intCast(u32, index_val.toUnsignedInt());
-            return sema.tupleFieldPtr(block, array_ptr, index, src, elem_index_src);
+            return sema.tupleFieldPtr(block, src, indexable_ptr, elem_index_src, index);
         },
         else => unreachable,
     }
@@ -16876,90 +16866,66 @@ fn elemVal(
     sema: *Sema,
     block: *Block,
     src: LazySrcLoc,
-    array: Air.Inst.Ref,
+    indexable: Air.Inst.Ref,
     elem_index_uncasted: Air.Inst.Ref,
     elem_index_src: LazySrcLoc,
 ) CompileError!Air.Inst.Ref {
-    const array_src = src; // TODO better source location
-    const array_ty = sema.typeOf(array);
+    const indexable_src = src; // TODO better source location
+    const indexable_ty = sema.typeOf(indexable);
 
-    if (!array_ty.isIndexable()) {
-        return sema.fail(block, src, "array access of non-indexable type '{}'", .{array_ty});
+    if (!indexable_ty.isIndexable()) {
+        return sema.fail(block, src, "element access of non-indexable type '{}'", .{indexable_ty});
     }
 
     // TODO in case of a vector of pointers, we need to detect whether the element
     // index is a scalar or vector instead of unconditionally casting to usize.
     const elem_index = try sema.coerce(block, Type.usize, elem_index_uncasted, elem_index_src);
 
-    switch (array_ty.zigTypeTag()) {
-        .Pointer => switch (array_ty.ptrSize()) {
-            .Slice => {
-                const maybe_slice_val = try sema.resolveDefinedValue(block, array_src, array);
-                const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
-                const runtime_src = if (maybe_slice_val) |slice_val| rs: {
-                    const index_val = maybe_index_val orelse break :rs elem_index_src;
-                    const index = @intCast(usize, index_val.toUnsignedInt());
-
-                    const elem_ty = array_ty.elemType2();
-
-                    var payload: Value.Payload.ElemPtr = .{ .data = .{
-                        .array_ptr = slice_val.slicePtr(),
-                        .elem_ty = elem_ty,
-                        .index = index,
-                    } };
-                    const elem_ptr_val = Value.initPayload(&payload.base);
-
-                    if (try sema.pointerDeref(block, array_src, elem_ptr_val, array_ty)) |elem_val| {
-                        return sema.addConstant(elem_ty, elem_val);
-                    }
-                    break :rs array_src;
-                } else array_src;
-
-                try sema.requireRuntimeBlock(block, runtime_src);
-                return block.addBinOp(.slice_elem_val, array, elem_index);
-            },
+    switch (indexable_ty.zigTypeTag()) {
+        .Pointer => switch (indexable_ty.ptrSize()) {
+            .Slice => return sema.elemValSlice(block, indexable_src, indexable, elem_index_src, elem_index),
             .Many, .C => {
-                const maybe_array_val = try sema.resolveDefinedValue(block, array_src, array);
+                const maybe_indexable_val = try sema.resolveDefinedValue(block, indexable_src, indexable);
                 const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
 
                 const runtime_src = rs: {
-                    const array_val = maybe_array_val orelse break :rs array_src;
+                    const indexable_val = maybe_indexable_val orelse break :rs indexable_src;
                     const index_val = maybe_index_val orelse break :rs elem_index_src;
                     const index = @intCast(usize, index_val.toUnsignedInt());
-                    const elem_ty = array_ty.elemType2();
+                    const elem_ty = indexable_ty.elemType2();
 
                     var payload: Value.Payload.ElemPtr = .{ .data = .{
-                        .array_ptr = array_val,
+                        .array_ptr = indexable_val,
                         .elem_ty = elem_ty,
                         .index = index,
                     } };
                     const elem_ptr_val = Value.initPayload(&payload.base);
 
-                    if (try sema.pointerDeref(block, array_src, elem_ptr_val, array_ty)) |elem_val| {
+                    if (try sema.pointerDeref(block, indexable_src, elem_ptr_val, indexable_ty)) |elem_val| {
                         return sema.addConstant(elem_ty, elem_val);
                     }
-                    break :rs array_src;
+                    break :rs indexable_src;
                 };
 
                 try sema.requireRuntimeBlock(block, runtime_src);
-                return block.addBinOp(.ptr_elem_val, array, elem_index);
+                return block.addBinOp(.ptr_elem_val, indexable, elem_index);
             },
             .One => {
-                assert(array_ty.childType().zigTypeTag() == .Array); // Guaranteed by isIndexable
-                const elem_ptr = try sema.elemPtr(block, array_src, array, elem_index, elem_index_src);
-                return sema.analyzeLoad(block, array_src, elem_ptr, elem_index_src);
+                assert(indexable_ty.childType().zigTypeTag() == .Array); // Guaranteed by isIndexable
+                const elem_ptr = try sema.elemPtr(block, indexable_src, indexable, elem_index, elem_index_src);
+                return sema.analyzeLoad(block, indexable_src, elem_ptr, elem_index_src);
             },
         },
-        .Array => return elemValArray(sema, block, array, elem_index, array_src, elem_index_src),
+        .Array => return elemValArray(sema, block, indexable_src, indexable, elem_index_src, elem_index),
         .Vector => {
             // TODO: If the index is a vector, the result should be a vector.
-            return elemValArray(sema, block, array, elem_index, array_src, elem_index_src);
+            return elemValArray(sema, block, indexable_src, indexable, elem_index_src, elem_index);
         },
         .Struct => {
             // Tuple field access.
             const index_val = try sema.resolveConstValue(block, elem_index_src, elem_index);
             const index = @intCast(u32, index_val.toUnsignedInt());
-            return tupleField(sema, block, array, index, array_src, elem_index_src);
+            return tupleField(sema, block, indexable_src, indexable, elem_index_src, index);
         },
         else => unreachable,
     }
@@ -16968,22 +16934,26 @@ fn elemVal(
 fn tupleFieldPtr(
     sema: *Sema,
     block: *Block,
+    tuple_ptr_src: LazySrcLoc,
     tuple_ptr: Air.Inst.Ref,
-    field_index: u32,
-    tuple_src: LazySrcLoc,
     field_index_src: LazySrcLoc,
+    field_index: u32,
 ) CompileError!Air.Inst.Ref {
     const tuple_ptr_ty = sema.typeOf(tuple_ptr);
     const tuple_ty = tuple_ptr_ty.childType();
-    const tuple = tuple_ty.tupleFields();
+    const tuple_fields = tuple_ty.tupleFields();
+
+    if (tuple_fields.types.len == 0) {
+        return sema.fail(block, field_index_src, "indexing into empty tuple", .{});
+    }
 
-    if (field_index > tuple.types.len) {
+    if (field_index >= tuple_fields.types.len) {
         return sema.fail(block, field_index_src, "index {d} outside tuple of length {d}", .{
-            field_index, tuple.types.len,
+            field_index, tuple_fields.types.len,
         });
     }
 
-    const field_ty = tuple.types[field_index];
+    const field_ty = tuple_fields.types[field_index];
     const target = sema.mod.getTarget();
     const ptr_field_ty = try Type.ptr(sema.arena, target, .{
         .pointee_type = field_ty,
@@ -16991,7 +16961,7 @@ fn tupleFieldPtr(
         .@"addrspace" = tuple_ptr_ty.ptrAddressSpace(),
     });
 
-    if (try sema.resolveMaybeUndefVal(block, tuple_src, tuple_ptr)) |tuple_ptr_val| {
+    if (try sema.resolveMaybeUndefVal(block, tuple_ptr_src, tuple_ptr)) |tuple_ptr_val| {
         return sema.addConstant(
             ptr_field_ty,
             try Value.Tag.field_ptr.create(sema.arena, .{
@@ -17002,29 +16972,33 @@ fn tupleFieldPtr(
         );
     }
 
-    try sema.requireRuntimeBlock(block, tuple_src);
+    try sema.requireRuntimeBlock(block, tuple_ptr_src);
     return block.addStructFieldPtr(tuple_ptr, field_index, ptr_field_ty);
 }
 
 fn tupleField(
     sema: *Sema,
     block: *Block,
-    tuple: Air.Inst.Ref,
-    field_index: u32,
     tuple_src: LazySrcLoc,
+    tuple: Air.Inst.Ref,
     field_index_src: LazySrcLoc,
+    field_index: u32,
 ) CompileError!Air.Inst.Ref {
     const tuple_ty = sema.typeOf(tuple);
-    const tuple_info = tuple_ty.tupleFields();
+    const tuple_fields = tuple_ty.tupleFields();
+
+    if (tuple_fields.types.len == 0) {
+        return sema.fail(block, field_index_src, "indexing into empty tuple", .{});
+    }
 
-    if (field_index > tuple_info.types.len) {
+    if (field_index >= tuple_fields.types.len) {
         return sema.fail(block, field_index_src, "index {d} outside tuple of length {d}", .{
-            field_index, tuple_info.types.len,
+            field_index, tuple_fields.types.len,
         });
     }
 
-    const field_ty = tuple_info.types[field_index];
-    const field_val = tuple_info.values[field_index];
+    const field_ty = tuple_fields.types[field_index];
+    const field_val = tuple_fields.values[field_index];
 
     if (field_val.tag() != .unreachable_value) {
         return sema.addConstant(field_ty, field_val); // comptime field
@@ -17043,57 +17017,221 @@ fn tupleField(
 fn elemValArray(
     sema: *Sema,
     block: *Block,
-    array: Air.Inst.Ref,
-    elem_index: Air.Inst.Ref,
     array_src: LazySrcLoc,
+    array: Air.Inst.Ref,
     elem_index_src: LazySrcLoc,
+    elem_index: Air.Inst.Ref,
 ) CompileError!Air.Inst.Ref {
     const array_ty = sema.typeOf(array);
-    if (try sema.resolveMaybeUndefVal(block, array_src, array)) |array_val| {
-        const elem_ty = array_ty.childType();
-        if (array_val.isUndef()) return sema.addConstUndef(elem_ty);
-        const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
+    const array_sent = array_ty.sentinel() != null;
+    const array_len = array_ty.arrayLen();
+    const array_len_s = array_len + @boolToInt(array_sent);
+    const elem_ty = array_ty.childType();
+
+    if (array_len_s == 0) {
+        return sema.fail(block, elem_index_src, "indexing into empty array", .{});
+    }
+
+    const maybe_undef_array_val = try sema.resolveMaybeUndefVal(block, array_src, array);
+    // index must be defined since it can access out of bounds
+    const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
+
+    if (maybe_index_val) |index_val| {
+        const index = @intCast(usize, index_val.toUnsignedInt());
+        if (index >= array_len_s) {
+            const sentinel_label: []const u8 = if (array_sent) " +1 (sentinel)" else "";
+            return sema.fail(block, elem_index_src, "index {d} outside array of length {d}{s}", .{ index, array_len, sentinel_label });
+        }
+    }
+    if (maybe_undef_array_val) |array_val| {
+        if (array_val.isUndef()) {
+            return sema.addConstUndef(elem_ty);
+        }
         if (maybe_index_val) |index_val| {
             const index = @intCast(usize, index_val.toUnsignedInt());
-            const len = array_ty.arrayLenIncludingSentinel();
-            if (index >= len) {
-                return sema.fail(block, elem_index_src, "index {d} outside array of length {d}", .{
-                    index, len,
-                });
-            }
             const elem_val = try array_val.elemValue(sema.arena, index);
             return sema.addConstant(elem_ty, elem_val);
         }
     }
-    try sema.requireRuntimeBlock(block, array_src);
+
+    const runtime_src = if (maybe_undef_array_val != null) elem_index_src else array_src;
+    try sema.requireRuntimeBlock(block, runtime_src);
+    if (block.wantSafety()) {
+        // Runtime check is only needed if unable to comptime check
+        if (maybe_index_val == null) {
+            const len_inst = try sema.addIntUnsigned(Type.usize, array_len);
+            const cmp_op: Air.Inst.Tag = if (array_sent) .cmp_lte else .cmp_lt;
+            const is_in_bounds = try block.addBinOp(cmp_op, elem_index, len_inst);
+            try sema.addSafetyCheck(block, is_in_bounds, .index_out_of_bounds);
+        }
+    }
     return block.addBinOp(.array_elem_val, array, elem_index);
 }
 
 fn elemPtrArray(
     sema: *Sema,
     block: *Block,
-    src: LazySrcLoc,
+    array_ptr_src: LazySrcLoc,
     array_ptr: Air.Inst.Ref,
-    elem_index: Air.Inst.Ref,
     elem_index_src: LazySrcLoc,
+    elem_index: Air.Inst.Ref,
 ) CompileError!Air.Inst.Ref {
+    const target = sema.mod.getTarget();
     const array_ptr_ty = sema.typeOf(array_ptr);
+    const array_ty = array_ptr_ty.childType();
+    const array_sent = array_ty.sentinel() != null;
+    const array_len = array_ty.arrayLen();
+    const array_len_s = array_len + @boolToInt(array_sent);
+    const elem_ptr_ty = try array_ptr_ty.elemPtrType(sema.arena, target);
+
+    if (array_len_s == 0) {
+        return sema.fail(block, elem_index_src, "indexing into empty array", .{});
+    }
+
+    const maybe_undef_array_ptr_val = try sema.resolveMaybeUndefVal(block, array_ptr_src, array_ptr);
+    // index must be defined since it can index out of bounds
+    const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
+
+    if (maybe_index_val) |index_val| {
+        const index = @intCast(usize, index_val.toUnsignedInt());
+        if (index >= array_len_s) {
+            const sentinel_label: []const u8 = if (array_sent) " +1 (sentinel)" else "";
+            return sema.fail(block, elem_index_src, "index {d} outside array of length {d}{s}", .{ index, array_len, sentinel_label });
+        }
+    }
+    if (maybe_undef_array_ptr_val) |array_ptr_val| {
+        if (array_ptr_val.isUndef()) {
+            return sema.addConstUndef(elem_ptr_ty);
+        }
+        if (maybe_index_val) |index_val| {
+            const index = @intCast(usize, index_val.toUnsignedInt());
+            const elem_ptr = try array_ptr_val.elemPtr(array_ptr_ty, sema.arena, index);
+            return sema.addConstant(elem_ptr_ty, elem_ptr);
+        }
+    }
+
+    const runtime_src = if (maybe_undef_array_ptr_val != null) elem_index_src else array_ptr_src;
+    try sema.requireRuntimeBlock(block, runtime_src);
+    if (block.wantSafety()) {
+        // Runtime check is only needed if unable to comptime check
+        if (maybe_index_val == null) {
+            const len_inst = try sema.addIntUnsigned(Type.usize, array_len);
+            const cmp_op: Air.Inst.Tag = if (array_sent) .cmp_lte else .cmp_lt;
+            const is_in_bounds = try block.addBinOp(cmp_op, elem_index, len_inst);
+            try sema.addSafetyCheck(block, is_in_bounds, .index_out_of_bounds);
+        }
+    }
+    return block.addPtrElemPtr(array_ptr, elem_index, elem_ptr_ty);
+}
+
+fn elemValSlice(
+    sema: *Sema,
+    block: *Block,
+    slice_src: LazySrcLoc,
+    slice: Air.Inst.Ref,
+    elem_index_src: LazySrcLoc,
+    elem_index: Air.Inst.Ref,
+) CompileError!Air.Inst.Ref {
+    const slice_ty = sema.typeOf(slice);
+    const slice_sent = slice_ty.sentinel() != null;
+    const elem_ty = slice_ty.elemType2();
+    var runtime_src = slice_src;
+
+    // slice must be defined since it can dereferenced as null
+    const maybe_slice_val = try sema.resolveDefinedValue(block, slice_src, slice);
+    // index must be defined since it can index out of bounds
+    const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
+
+    if (maybe_slice_val) |slice_val| {
+        runtime_src = elem_index_src;
+        const slice_len = slice_val.sliceLen();
+        const slice_len_s = slice_len + @boolToInt(slice_sent);
+        if (slice_len_s == 0) {
+            return sema.fail(block, elem_index_src, "indexing into empty slice", .{});
+        }
+        if (maybe_index_val) |index_val| {
+            const index = @intCast(usize, index_val.toUnsignedInt());
+            if (index >= slice_len_s) {
+                const sentinel_label: []const u8 = if (slice_sent) " +1 (sentinel)" else "";
+                return sema.fail(block, elem_index_src, "index {d} outside slice of length {d}{s}", .{ index, slice_len, sentinel_label });
+            }
+            var elem_ptr_pl: Value.Payload.ElemPtr = .{ .data = .{
+                .array_ptr = slice_val.slicePtr(),
+                .elem_ty = elem_ty,
+                .index = index,
+            } };
+            const elem_ptr_val = Value.initPayload(&elem_ptr_pl.base);
+            if (try sema.pointerDeref(block, slice_src, elem_ptr_val, slice_ty)) |elem_val| {
+                return sema.addConstant(elem_ty, elem_val);
+            }
+            runtime_src = slice_src;
+        }
+    }
+
+    try sema.requireRuntimeBlock(block, runtime_src);
+    if (block.wantSafety()) {
+        const len_inst = if (maybe_slice_val) |slice_val|
+            try sema.addIntUnsigned(Type.usize, slice_val.sliceLen())
+        else
+            try block.addTyOp(.slice_len, Type.usize, slice);
+        const cmp_op: Air.Inst.Tag = if (slice_sent) .cmp_lte else .cmp_lt;
+        const is_in_bounds = try block.addBinOp(cmp_op, elem_index, len_inst);
+        try sema.addSafetyCheck(block, is_in_bounds, .index_out_of_bounds);
+    }
+    return block.addBinOp(.slice_elem_val, slice, elem_index);
+}
+
+fn elemPtrSlice(
+    sema: *Sema,
+    block: *Block,
+    slice_src: LazySrcLoc,
+    slice: Air.Inst.Ref,
+    elem_index_src: LazySrcLoc,
+    elem_index: Air.Inst.Ref,
+) CompileError!Air.Inst.Ref {
     const target = sema.mod.getTarget();
-    const result_ty = try array_ptr_ty.elemPtrType(sema.arena, target);
-
-    if (try sema.resolveDefinedValue(block, src, array_ptr)) |array_ptr_val| {
-        if (try sema.resolveDefinedValue(block, elem_index_src, elem_index)) |index_val| {
-            // Both array pointer and index are compile-time known.
-            const index_u64 = index_val.toUnsignedInt();
-            // @intCast here because it would have been impossible to construct a value that
-            // required a larger index.
-            const elem_ptr = try array_ptr_val.elemPtr(array_ptr_ty, sema.arena, @intCast(usize, index_u64));
-            return sema.addConstant(result_ty, elem_ptr);
+    const slice_ty = sema.typeOf(slice);
+    const slice_sent = slice_ty.sentinel() != null;
+    const elem_ptr_ty = try slice_ty.elemPtrType(sema.arena, target);
+
+    const maybe_undef_slice_val = try sema.resolveMaybeUndefVal(block, slice_src, slice);
+    // index must be defined since it can index out of bounds
+    const maybe_index_val = try sema.resolveDefinedValue(block, elem_index_src, elem_index);
+
+    if (maybe_undef_slice_val) |slice_val| {
+        if (slice_val.isUndef()) {
+            return sema.addConstUndef(elem_ptr_ty);
+        }
+        const slice_len = slice_val.sliceLen();
+        const slice_len_s = slice_len + @boolToInt(slice_sent);
+        if (slice_len_s == 0) {
+            return sema.fail(block, elem_index_src, "indexing into empty slice", .{});
+        }
+        if (maybe_index_val) |index_val| {
+            const index = @intCast(usize, index_val.toUnsignedInt());
+            if (index >= slice_len_s) {
+                const sentinel_label: []const u8 = if (slice_sent) " +1 (sentinel)" else "";
+                return sema.fail(block, elem_index_src, "index {d} outside slice of length {d}{s}", .{ index, slice_len, sentinel_label });
+            }
+            const elem_ptr_val = try slice_val.elemPtr(slice_ty, sema.arena, index);
+            return sema.addConstant(elem_ptr_ty, elem_ptr_val);
         }
     }
-    // TODO safety check for array bounds
-    try sema.requireRuntimeBlock(block, src);
-    return block.addPtrElemPtr(array_ptr, elem_index, result_ty);
+
+    const runtime_src = if (maybe_undef_slice_val != null) elem_index_src else slice_src;
+    try sema.requireRuntimeBlock(block, runtime_src);
+    if (block.wantSafety()) {
+        const len_inst = len: {
+            if (maybe_undef_slice_val) |slice_val|
+                if (!slice_val.isUndef())
+                    break :len try sema.addIntUnsigned(Type.usize, slice_val.sliceLen());
+            break :len try block.addTyOp(.slice_len, Type.usize, slice);
+        };
+        const cmp_op: Air.Inst.Tag = if (slice_sent) .cmp_lte else .cmp_lt;
+        const is_in_bounds = try block.addBinOp(cmp_op, elem_index, len_inst);
+        try sema.addSafetyCheck(block, is_in_bounds, .index_out_of_bounds);
+    }
+    return block.addSliceElemPtr(slice, elem_index, elem_ptr_ty);
 }
 
 fn coerce(
@@ -18000,7 +18138,7 @@ fn storePtr2(
         for (tuple.types) |_, i_usize| {
             const i = @intCast(u32, i_usize);
             const elem_src = operand_src; // TODO better source location
-            const elem = try tupleField(sema, block, uncasted_operand, i, operand_src, elem_src);
+            const elem = try tupleField(sema, block, operand_src, uncasted_operand, elem_src, i);
             const elem_index = try sema.addIntUnsigned(Type.usize, i);
             const elem_ptr = try sema.elemPtr(block, ptr_src, ptr, elem_index, elem_src);
             try sema.storePtr2(block, src, elem_ptr, elem_src, elem, elem_src, .store);
@@ -18885,7 +19023,7 @@ fn coerceArrayLike(
             try Value.Tag.int_u64.create(sema.arena, i),
         );
         const elem_src = inst_src; // TODO better source location
-        const elem_ref = try elemValArray(sema, block, inst, index_ref, inst_src, elem_src);
+        const elem_ref = try elemValArray(sema, block, inst_src, inst, elem_src, index_ref);
         const coerced = try sema.coerce(block, dest_elem_ty, elem_ref, elem_src);
         element_refs[i] = coerced;
         if (runtime_src == null) {
@@ -18942,7 +19080,7 @@ fn coerceTupleToArray(
     for (element_vals) |*elem, i_usize| {
         const i = @intCast(u32, i_usize);
         const elem_src = inst_src; // TODO better source location
-        const elem_ref = try tupleField(sema, block, inst, i, inst_src, elem_src);
+        const elem_ref = try tupleField(sema, block, inst_src, inst, elem_src, i);
         const coerced = try sema.coerce(block, dest_elem_ty, elem_ref, elem_src);
         element_refs[i] = coerced;
         if (runtime_src == null) {
@@ -19042,7 +19180,7 @@ fn coerceTupleToStruct(
         if (field.is_comptime) {
             return sema.fail(block, dest_ty_src, "TODO: implement coercion from tuples to structs when one of the destination struct fields is comptime", .{});
         }
-        const elem_ref = try tupleField(sema, block, inst, i, inst_src, field_src);
+        const elem_ref = try tupleField(sema, block, inst_src, inst, field_src, i);
         const coerced = try sema.coerce(block, field.ty, elem_ref, field_src);
         field_refs[field_index] = coerced;
         if (runtime_src == null) {