Skip to content

typechecker: a local bound to s[i] is a byte (#3479) - #3505

Closed
paul-hammant wants to merge 2 commits into
mainfrom
fix/3479-string-index-local-type
Closed

paul-hammant wants to merge 2 commits into
mainfrom
fix/3479-string-index-local-type

Conversation

@paul-hammant

Copy link
Copy Markdown
Collaborator

Fixes #3479.

Reworks Jules's draft (paul-hammant#49, Jules task 7456359029880539017).

The bug

c = s[i] (a local, parameter, field or returned string) left c untyped. collect_expression_constraints set an element type for AST_ARRAY_ACCESS only when the base was TYPE_ARRAY, so a string base fell through, and codegen defaulted the local to int with warning: unresolved type in codegen, defaulting to int. That fails make check-examples, which blocks the Rosetta Caesar cipher in #3477. return c as int on such a local was refused too ("cannot cast unknown to int").

The fix (Jules's, 4 lines)

A string base types the read as byte, which is what the language reference says s[i] reads.

This changes behaviour, and it is consistent with byte[]:

  • a high byte reads 195, not -61;
  • arithmetic stored back into the local wraps: c = s[0]; c = c - 100 gives 244 for "X", where the int default gave -12. A local bound to a byte[] element (x = b[0]; x = x - 70) already wraps the same way on main;
  • c = s[0]; c = 1000 is now a compile error (byte literal out of range);
  • s[0] + 1, s[0] == 88 and match c { 65..=90 -> ... } are unchanged. s[i] as int keeps a signed result.

The changelog says so, and the language reference now says s[i] reads a byte.

What changed from the draft

  • The test is an integration directory, not a regression file. The draft's tests/regression/test_issue3479_string_index_local.ae re-ran ./build/ae build --with=all from inside itself. That depended on the current directory, wrote into build/, and used --with=all, the backdoor AGENTS.md rules out. tests/integration/string_index_local_type/ has a fixture and a script. The script builds the fixture with no warning at all and runs it, and the sweep runs the fixture too.
  • The fixture pins the byte semantics: 88 - 100 wraps to 244, and as int gives -12.
  • The changelog says what the byte type means for arithmetic.

Verification

  • Negative control 1: the fixture built with main's compiler fails on return c as int ("cannot cast unknown to int").
  • Negative control 2: a copy without the casts fails the no-warning check, with four unresolved type in codegen, defaulting to int warnings.
  • With the fix: [PASS] string_index_local_type.
  • make test: passes.
  • make test-ae: 2071 passed, 2 failed. The two are http_server_h2 and windows_crt_symbols, which fail the same way on a clean main.
  • check-examples: clean apart from an untracked local directory.
  • Gates: ae fmt, check-docs, check-changelog, check-tests, check-warnings and check-raw-calls all pass.

🤖 Generated with Claude Code

google-labs-jules Bot and others added 2 commits November 1, 2026 19:15
When indexing a string (`s[i]`), type inference was leaving the node type
unset because it only handled `TYPE_ARRAY`. As a result, `infer_type`
returned `TYPE_UNKNOWN`, leaving any local bound to `s[i]` without a known
type until codegen defaulted it to `int` with a warning.

Infer string indexing as `TYPE_BYTE` (an unsigned 8-bit integer /
`unsigned char` in C), matching the language definition in
docs/language-reference.md ("s[i] reads one [byte]"), existing typed
contexts, and unsigned byte semantics.

Add regression test tests/regression/test_issue3479_string_index_local.ae
covering all string sources (local, parameter, struct field, function
return value, StringSeq, string[] element), high-byte indexing
("\xC3"[0] == 195), arithmetic/comparison/range match, and zero warning
verification.

Co-authored-by: paul-hammant <82182+paul-hammant@users.noreply.github.com>
`c = s[i]` left the local untyped: inference set an element type only for
an array base, so a string base fell through and codegen defaulted the
local 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 (88 - 100 is 244, where the
int default gave -12); `s[i] as int` keeps a signed result.

The test is an integration directory: a fixture binding the read from a
local, a parameter, a field, a returned string and *StringSeq / string[]
elements, and a script that builds it with no warning and runs it.

Reworks Jules's draft (paul-hammant#49, Jules task
7456359029880539017): the fix is Jules's; the test no longer re-runs
`./build/ae build --with=all` from inside itself, and the changelog and
language reference say what the byte type means for arithmetic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@paul-hammant

Copy link
Copy Markdown
Collaborator Author

Superseded by 235ea27 (in 0.871.0), which fixed #3479 on main with s[i] typed as an int from 0 to 255 rather than the byte this PR uses (a byte local wraps on arithmetic and refused uint32 u = s[i]). Closing.

@paul-hammant
paul-hammant deleted the fix/3479-string-index-local-type branch November 2, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

c = s[i] on a string warns 'unresolved type in codegen, defaulting to int'

1 participant