Skip to content
Closed
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
4 changes: 4 additions & 0 deletions compiler/analysis/type_inference.c

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion docs/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ main() {
- **`s[i]` is bounds-checked.** An index outside `0 ..< s.len` is a runtime panic naming the source line, the index and the length (`slice index 5 out of range for length 3`). It is an ordinary panic: `try` catches it, and an actor step that trips it dies cleanly. A negative index is out of range, never a read before the buffer. An index into a fixed-size `T[N]` array is checked the same way, against `N` ([Array Types](#array-types)).
- **`s[lo..hi]`** is the half-open sub-slice `[lo, hi)`, checked the same way (`lo <= hi <= s.len`). `s[lo..]` runs to the end, `s[..hi]` from the start, `s[..]` is the whole thing. The base may be a slice or a `T[N]` array; the result is always a `T[]` over the *same* elements, so a write through it is visible through the original.
- **A `T[N]` array converts to `T[]` implicitly** wherever a slice is expected: a parameter, a `T[]`-typed binding or field, a `return` from a `-> T[]` function. The length travels with it. `null`, and the empty literal `[]`, convert to the empty slice: `.len` is `0`, it views no storage, and it compares equal to `null`. So `s == []` is `s == null` (it asks whether `s` views any storage; test `s.len == 0` for emptiness), a C function's `T[]` parameter takes `[]` as the null pointer, and an if-expression or a `match` given to a `T[]` converts each arm on its own. A module-level `var` or `const` of type `T[]` takes either as its initial value.
- **A string is not a slice.** Passing one where a `T[]` is expected (a parameter, a binding, a `return`) is a type error; `string.bytes(s)` is the bounded, read-only `byte[]` view of a string's bytes, and it decays to their pointer at a C extern like any slice. Reading it costs what any slice's read does; a store through it (an element store, `+=`, `++`), or through any view made from it (a sub-slice, a pattern's tail), in this function or one it is given to, panics with "a write through a read-only view of a string", and one the checker can see through a local bound to it is a compile error (E0200) (#3025). A string's own bytes are read-only too: `s[i]` reads one, and a write (`s[i] = v`, `s[i] += v`, `s[i]++`) is a compile error (E0200). Build the new text as a new string, or work on the bytes in a `byte[N]` array of your own.
- **A string is not a slice.** Passing one where a `T[]` is expected (a parameter, a binding, a `return`) is a type error; `string.bytes(s)` is the bounded, read-only `byte[]` view of a string's bytes, and it decays to their pointer at a C extern like any slice. Reading it costs what any slice's read does; a store through it (an element store, `+=`, `++`), or through any view made from it (a sub-slice, a pattern's tail), in this function or one it is given to, panics with "a write through a read-only view of a string", and one the checker can see through a local bound to it is a compile error (E0200) (#3025). A string's own bytes are read-only too: `s[i]` reads one (a `byte`, so `c = s[i]` binds a `byte` local), and a write (`s[i] = v`, `s[i] += v`, `s[i]++`) is a compile error (E0200). Build the new text as a new string, or work on the bytes in a `byte[N]` array of your own.
- **A slice decays to its pointer** wherever a `ptr` is expected, exactly as a `T[N]` array does: a `ptr` parameter, a C extern's parameter (including one declared `T[]`), a comparison against `null`, and pointer arithmetic (`s + n` is a `ptr` to element `n`). `s as ptr` spells the same conversion explicitly, for a `ptr` return or binding. The length is dropped at that boundary; nothing else about the extern ABI changes.
- **`make([]T, n)`** allocates `n` zeroed elements and returns them as a bounded slice. The local that holds it releases it when it goes out of scope ([Slice ownership](#slice-ownership)), and `free(s)` releases it early, and first what the elements hold: each element is a place a store into it takes a reference for (a string, a closure, a struct that holds one), so the free gives each back, as a struct's destroy does its fields (#3159). The elements of C memory viewed as a slice hold no reference, and its free releases only the memory. Only the slice that holds such a buffer, or a view of a raw pointer the program owns from its start (`(p as T[])[0..n]`), may be freed: a view owns nothing. `free` of a sub-slice (`b[2..4]`, `b[1..]`, a list pattern's tail), of an array viewed as a slice, of `string.bytes(s)`, or of an array `T[N]`, is a compile error where the checker sees it (a local bound once to one, or the expression itself), and a named panic at run time anywhere else (`free of a view of elements another slice or an array holds, which owns nothing`). Before, the allocator was given a pointer it never handed out. In a module that defines a `free` of its own (`bignum.free`, `json.free`), `free(s)` of a slice is still this release, unless the module's `free` itself takes a slice: the module's function would only see the slice's pointer (#3116).
- **`T[][]` is a slice of slices**, and `T[][][]` one deeper: each further `[]` wraps the type once more, in any position a type is written (a parameter, a return type, a field, a typed local, a function type). `make([]int[], n)` builds one; `g[i] = row` stores a row, `g[i][j]` reads an element and `g[i].len` is a row's length. The outer slice owns its rows: a store into `g[i]` releases the row it replaces, and releasing the outer slice (its owner's scope exit, or `free(g)`) releases every row first; `free(g[i])` releases a row early and leaves it empty. A row read out (`r = g[i]`) is a view of it. Only the empty `[]` repeats; there is no `T[][3]`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
- **A local bound to one byte of a string is a `byte`** (#3479). `c = s[i]` (a
local, parameter, field or returned string) left the local untyped until
codegen defaulted it to `int`, with "unresolved type in codegen, defaulting
to int", which fails `make check-examples`. The read is a byte, so the local
is one, as a local bound to a `byte[]` element is: a high byte reads 195
(not -61), and arithmetic stored back into the local wraps (`c = s[0]; c =
c - 100` is 244 for "X"), where the int default gave -12. Widen with `as
int` (`s[0] as int - 100`) to keep a signed result.
77 changes: 77 additions & 0 deletions tests/integration/string_index_local_type/string_index_local.ae
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// #3479: a local bound to one byte of a string (`c = s[i]`) is typed by the
// read, a byte, as a local bound to an element of a byte[] is. It used to be
// left unresolved until codegen defaulted it to int, with a warning.
// test_string_index_local_type.sh builds this with no warning and runs it;
// the sweep runs it as well.
import std.string

extern exit(code: int)

struct Holder {
text: string
}

returned() -> string {
return "ABC"
}

from_param(text: string, i: int) -> int {
c = text[i]
return c as int
}

check(ok: bool, what: string) {
if !ok {
println("FAIL: ${what}")
exit(1)
}
}

main() {
s = "XYZ"
c = s[0]
check(c == 88, "a local's s[0] is 88")

check(from_param("PARAM", 0) == 80, "a parameter's text[0] is 80")

h = Holder { text: "FIELD" }
f = h.text[0]
check(f == 70, "a field's text[0] is 70")

r = returned()[0]
check(r == 65, "a returned string's [0] is 65")

let seq: *StringSeq = ["SEQ1", "SEQ2"]
head = string.seq_head(seq)
q = head[0]
check(q == 83, "a *StringSeq head's [0] is 83")

arr = ["ARR1", "ARR2"]
a = arr[0][0]
check(a == 65, "a string[] element's [0] is 65")

// A byte is unsigned: a high byte reads 195, not -61.
high = "\xC3"
b = high[0]
check(b == 195, "\"\\xC3\"[0] is 195")

d = s[0] + 1
check(d == 89, "s[0] + 1 is 89")
check(s[0] == 88, "s[0] == 88")

upper = false
match c {
65..=90 -> upper = true
_ -> upper = false
}
check(upper, "a match over 65..=90 takes the byte")

// The local is a byte, so arithmetic stored back into it wraps, as a
// byte[] element's local does; widen with `as int` to go below 0.
w = s[0]
w = w - 100
check(w == 244, "a byte local's 88 - 100 wraps to 244")
check(s[0] as int - 100 == -12, "s[0] as int - 100 is -12")

println("PASS: string_index_local_type")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/bin/sh
# #3479: a local bound to one byte of a string (`c = s[0]`, `c = text[i]`)
# was left untyped by inference, and codegen defaulted it to int with
# "warning: unresolved type in codegen, defaulting to int", which fails
# `make check-examples` (warnings are errors there). The read is a byte, so
# the local is a byte.
#
# string_index_local.ae binds the read from a local, a parameter, a field, a
# returned string and *StringSeq / string[] elements, and checks the values
# (a high byte is 195, not -61). Building it must print no warning at all;
# running it must pass.

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
AE="$ROOT/build/ae"
export AETHER_HOME="$ROOT"

[ -x "$AE" ] || [ -x "$AE.exe" ] || { echo " [SKIP] string_index_local_type: ae not built"; exit 0; }

TMP="$(mktemp -d)"
trap 'rm -rf "$TMP"' EXIT
cp "$SCRIPT_DIR/string_index_local.ae" "$TMP/"

if ! (cd "$TMP" && "$AE" build string_index_local.ae -o prog > build.log 2>&1); then
echo " [FAIL] string_index_local_type: build failed"
sed 's/^/ /' "$TMP/build.log" | head -20
exit 1
fi
if grep -q "warning" "$TMP/build.log"; then
echo " [FAIL] string_index_local_type: the build warned"
sed 's/^/ /' "$TMP/build.log" | head -20
exit 1
fi
out="$(cd "$TMP" && ./prog 2>&1)"
case "$out" in
*"PASS: string_index_local_type"*) ;;
*) echo " [FAIL] string_index_local_type: the program failed"
echo "$out" | sed 's/^/ /'
exit 1 ;;
esac
echo " [PASS] string_index_local_type: s[i] binds a byte local with no warning, from every source"
Loading