Skip to content

Bugfix batch August 28 2026 - #2557

Merged
SchoolyB merged 27 commits into
mainfrom
v0.5.4
Aug 28, 2026
Merged

Bugfix batch August 28 2026#2557
SchoolyB merged 27 commits into
mainfrom
v0.5.4

Conversation

@SchoolyB

@SchoolyB SchoolyB commented Aug 28, 2026

Copy link
Copy Markdown
Member

Summary

This PR contains several bugfixes, major improvements to overall test coverage as well as a new make benchmark CLI command among docs imrpovements

Bugfixes

Performance, Optimizations & Refactors

Tests

Docs

  • Update TESTING. STANDARD, README & ERRORS .md files

strptime is hidden by glibc unless _XOPEN_SOURCE is defined before
<time.h>. On Ubuntu CI this caused an implicit declaration in
stdlib/time.c: the char* return was truncated to int, leaving end
pointing at garbage. time_parse crashed at runtime and the P0105
impossible-date test leaked a cc error instead of panicking. macOS
declares strptime unconditionally, so all macOS tests passed.
…it (#2545)

File-scope variable initializers that lower to runtime calls (negative
integer overflow checks, struct literals with array/map fields, string
interpolation) were emitted inline, which C rejects as non-constant
file-scope initializers. Emit a zero-initialized global and defer the
real initializer into the global-init buffer, matching the existing
emit_vardecl_array/emit_vardecl_map handling.
…ence parameter (#2546)

emit_mutable_call_argument() emitted `&<raw name>` for a bare identifier
argument, skipping the name resolution that emit_label() applies. A
module-level variable is declared under its mangled name, so the
reference failed to compile with 'use of undeclared identifier'. Resolve
the name through codegen_resolve_ref() before taking its address.
…own module (#2537)

resolve_member_expr() only did a plain scope_lookup for a bare-label
object and gave up on a miss, so a module-level struct variable
referenced inside its declaring module resolved to null and
addr(MODVAR.field) typed as ^unknown. Apply the same module-mangled
symbol fallback the NODE_LABEL value path uses.
…unc signature positions (#2524)

resolve_type_alias() only recursed into pointer (^T) and dynamic-array
([T]) type strings, so an alias used as a map key/value, a fixed-size
array element (`[I,N]`), or a func signature parameter/return
(`func(I)->I`) kept its alias spelling while the value's inferred type
used the underlying name, producing spurious E3001/E3066 mismatches.
Recurse into all three composite forms and re-resolve alias chains whose
target is itself a map or func type.
…ith t{} (#2467)

The lowercase struct-literal parse was fixed in #2500; the one shape it
left untested is a lowercase type parameter used as t{} in a generic
function body. Add that case to the existing lowercase_type_names test.
@github-actions github-actions Bot added documentation Improvements or additions to documentation typechecker Related to type checking and validation stdlib General standard library issues tests Related to unit tests or test infrastructure error-messages Related to improving error messages CI/CD Continuous Integration/Continuous Deployment cli Command-line interface related grayc Related to the grayscale compiler core or its internal tooling labels Aug 28, 2026
Since mut became optional, 'foobar int' with no value compiles and
silently zero-initializes, so a dropped '= value' has no signal. Emit
W1004 on any mut-class declaration that has a type annotation but no
value, formatting the type's zero literal into the message. const is
unaffected (E2011 already requires a value).
…ps (#2498)

Add pass tests for previously-uncovered builtins (char_count, char, sleep_s/ms/ns, cast to f32/f64/u32), defer execution semantics (LIFO, arg-eval-at-exit, nested activations), and bitwise ops on sized integer types. Move the @runtime introspection test into pass/stdlib/. Give 12 assertion-free pass/core files real assertions and a SOME TESTS FAILED footer so a wrong value is no longer invisible to the runner.
… on wrong output (#2498)

22 files incremented a failed counter but never surfaced it to the runner (no panic, no SOME TESTS FAILED, exit 0) — add the standard footer so a wrong value fails the run. Rewrite 7 nominal tests (all_imports_used, cast_array, char_byte_array_literals, const_arithmetic, escaped_interpolation, for_blank_identifier, local_const_array_size) with real assertions. cast_array is narrowed to same-width element casts; cross-width [int]->[u8] casts reinterpret the buffer instead of converting per element.
Rewrite 13 nominal pass/core tests (discard_attribute, doc_test, fixed_size_array_of_maps, function_scoped_using, map_field_ptr_deref, mixed_shared_type, pointer_correct_struct_arg, shared_type_named_return, struct_wildcard_monomorphize, type_param_generic, both keyword_alias_consistent files) with real assertions; the eprint/eprintln smoke tests assert only that every type runs. Add the standard SOME TESTS FAILED footer to 26 files that tracked a failed counter but never surfaced it to the runner. pass/core now has no test that passes silently on wrong output.
Extend existing per-module pass/stdlib tests to exercise every stdlib function that had no test: atomic and/or/xor, arrays/maps is_equal, binary 128/256-bit encode/decode (both byte orders), mem zero/fill/raw_copy, os set_env/unset_env, uuid generate_hyphenated, time tick/elapsed_ms/weekday, json parse/pretty_print, threads spawn_arg, io temp_dir/temp_file/write_bytes/append_bytes. Only net accept/receive/send/set_timeout remain — they need a live loopback socket pair the integration harness can't set up cleanly.
C promotes uint8_t/uint16_t to int before applying ~, producing a negative
value that fails the narrowing range check on assignment. Codegen already
cast byte results to uint8_t; extend the same mask to u8 and u16.
…te (#2554)

spawn_thread never checked pthread_create's return, so an OS-level failure
(thread limit, memory pressure) left a handle with an uninitialized pthread_t,
a leaked state/ThreadArg, a stuck live count, and alive=1 forever. Now the
return code is checked: on failure the live count and alive flag are unwound,
both allocations freed, and P0108 is raised. The four mallocs go through a
thread_alloc wrapper that raises P0109 on OOM.
… int64_t (#2555)

cast(arr, [T]) stores its result packed at sizeof(T), but emit_index_expr and
emit_foreach_array only special-cased byte among the sub-8-byte types; every
other sized int (u8, u16, i16, i32, u32, ...) fell through to an int64_t read
that strided past the buffer. Both element-type switches now resolve TK_INT/
TK_UINT element types through gray_type_to_c_codegen.
The char builtin branch only rejected a string argument when it was a string
literal of length != 1. A length-1 literal or any non-literal string expression
passed through as a char and reached codegen, which emitted (int32_t)(GrayString)
and leaked a cc error. The branch now rejects any argument that is not an
integer kind (or char) with E3001.
…ssign (#2550)

m[key][i] and m[key][i] = v lowered to an address-of on the rvalue that the
map lookup statement-expression produces, which cc rejected. Both the read
(emit_index_expr) and the write (index assignment) paths now bind the lookup
result to a GrayArray temp first; the header is a view over the stored buffer
so element writes still land in the map's array. Covers plain and compound
assignment including the sized-int overflow check.
…ompound assign

The sized-int overflow-checked compound assignment path (a[i] += n) read the
element with a hardcoded int64_t GRAY_ARRAY_GET_AT, over-reading a packed
sub-8-byte array produced by cast(arr, [T]) and feeding garbage to the range
check. GET now uses the element's C width; SET keeps int64_t since its memcpy
length is the runtime elem_size, which stays safe for 8-byte literal slots.
…nteger (#2536)

A string-backed enum is a GrayString at runtime, but the cast allowlist
treated every TK_ENUM as int-backed, so cast(int, StringEnum) and
cast(StringEnum, int) passed the typechecker and reached clang as a cast
between an arithmetic type and a struct. Both int-backed enum rules now
require the enum not be string-backed, and string <-> string-backed enum
is allowed in their place (a pure GrayString reinterpretation in codegen).
Add benchmarks/ with a seven-workload suite timed end to end: JSON
round-trip, ray tracer, lexer, Game of Life, KV store, SHA-256, and a
compile-stress phase. run.sh builds a CLOCK_MONOTONIC stopwatch (runner.c),
generates deterministic fixtures (gen.sh), times compile and run phases,
prints a table, and writes benchmarks/results.json with machine info,
commit, and timestamp. Grayscale-only absolute numbers - no baseline, no
regression gate, no CI job. POSIX only; the make target is a no-op on
Windows. BENCH_RUNS / BENCH_COMPILE_RUNS override iteration counts;
fixtures are generated once and reused.

Wire benchmark into .PHONY, KNOWN_TARGETS, and help; allow-list
benchmarks/** in .gitignore while keeping .gen/ and results.json out; add
a pointer row to CONTRIBUTING.md.
…#2547)

Add 22 sections (28-49) to cli/tests.gray covering language features and
stdlib that the verification suite never exercised: error handling,
or_return (bare and fallback), ensure/defer LIFO, if/or/otherwise,
bitwise operators, compound assignment, not_in, recursion, type aliases,
nested composites, char<->int, scoped blocks, const locals, and
higher-order functions; plus extended coverage of strings, arrays, maps,
math, json (decode/round-trip/nested), strconv error slots, csv, and
runtime.

Notes:
- Section 34 exercises not_in only; a file may use just one alias of a
  keyword (E2088).
- Section 39 is 'Scoped blocks' rather than shadowing: real shadowing
  emits W2002 and the suite compiles warning-free.
- arrays.deduplicate is left out: it does not typecheck in an assignment
  position right now.

gray verify passes: 230 assertions, 0 failures.
)

run.sh now wipes .gen/ and regenerates it every run instead of printing a
'delete it yourself' message; the fixtures are deterministic and rebuild
in under a second.

Each fixture-reading workload (json_roundtrip, lexer, kvstore, sha256)
falls back to benchmarks/.gen next to its own source when BENCH_GEN is
unset, so 'gray build benchmarks/workloads/lexer.gray && ./lexer' works
from any directory instead of failing with 'cannot read fixture'.

Document the suite: a Learn More link in README.md, and a 'Running one
workload' section plus a BENCH_GEN row in benchmarks/README.md.
…#2556)

test_stdlib.c gains 30 tests across the three priority modules: json
(encode, decode, decode_result ok/err, round-trip, is_valid, pretty,
split_array), io (write/read/append/bytes round-trips in a portable temp
dir, read_lines, result-variant error slot, make/remove dir, and the pure
path helpers), and regex (is_valid, match, find, find_all, replace, split,
result-variant error slot). STDLIB_TEST_DEPS picks up json.o, io.o,
regex.o.

test_codegen.c gains 8 e2e cases for features with a codegen path but no
end-to-end coverage: bitwise operators, the defer keyword, string stdlib
(to_upper/to_lower/trim/replace/contains/split), a when multi-value arm,
maps.remove_key, tagged (associated-data) enums, struct field defaults,
and a generic <?> type parameter.

Unit suites: 130 -> 160 stdlib tests. E2e: 121 -> 129.
@SchoolyB
SchoolyB merged commit a93cfc4 into main Aug 28, 2026
@SchoolyB
SchoolyB deleted the v0.5.4 branch August 28, 2026 23:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI/CD Continuous Integration/Continuous Deployment cli Command-line interface related documentation Improvements or additions to documentation error-messages Related to improving error messages grayc Related to the grayscale compiler core or its internal tooling stdlib General standard library issues tests Related to unit tests or test infrastructure typechecker Related to type checking and validation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant