refactor: canonical failable syntax (T, !) — remove the bare -> T ! sugar

The trailing-`!`-after-the-value-type spelling (`-> T !`, `-> Tuple(A,B) !`) was a
redundant second way to write a failable return that the parser folded into the
same AST as the parenthesized `(T, !)` / `(A, B, !)` result list. Remove it so
there is ONE canonical spelling: the error channel always rides as the last slot
of the parenthesized list.

- parser: `parseFnReturnType` no longer folds a trailing `!` after a value type —
  it rejects it with a located diagnostic ("a failable return is written `(T, !)`
  … not `T !`"). This one chokepoint covers fn declarations, lambdas, fn-pointer
  types `(A) -> R`, and closure types `Closure(A) -> R`. The error-ONLY `-> !` /
  `-> !ErrSet` form is unaffected (parsed by parseTypeExpr as an error_type_expr).
- migrated every usage to canonical form across library/ + examples/ + issues/ +
  tests/: `-> T !E` → `-> (T, !E)`; the value-carrying `-> Tuple(A, B) !` (which
  FLATTENED to a multi-value failable) → `-> (A, B, !)`, preserving behavior. A
  genuine single-tuple-value failable stays `-> (Tuple(A,B), !)`.
- parser unit tests: the "bare form folds" tests become "bare form is rejected";
  canonical-form parse tests retained.
- docs: specs.md §12 + scattered refs and readme.md updated to the `(T, !)` form.

Behavior-preserving (the bare form was sugar for the same AST). Adversarial review
confirmed: rejection complete across all positions, every canonical form works on
both success/error paths, error-only `-> !` intact, no crashes. Full suite green
(unit tests + 850 corpus examples).
This commit is contained in:
agra
2026-06-27 18:11:20 +03:00
parent b322dcfe61
commit 213cedf0b5
53 changed files with 184 additions and 232 deletions

View File

@@ -321,7 +321,7 @@ write_object :: (obj: Object, sink: *Sink) -> !JsonError {
// bytes written. Raises `error.Overflow` if `dst` is too small (the
// partial contents of `dst` are then undefined — nothing is truncated
// silently). No allocation.
write_to_buffer :: (v: Value, dst: []u8) -> i64 !JsonError {
write_to_buffer :: (v: Value, dst: []u8) -> (i64, !JsonError) {
sink := Sink.{ dst = dst };
try write_value(v, @sink);
return sink.pos;
@@ -386,7 +386,7 @@ JsonParseError :: error { UnexpectedToken, UnexpectedEnd, BadEscape, BadNumber,
// Lowercase/uppercase hex nibble value (0..15) of an ASCII byte; a non-hex
// byte in a `\uXXXX` escape is a `BadEscape`.
hex_value :: (c: u8) -> i64 !JsonParseError {
hex_value :: (c: u8) -> (i64, !JsonParseError) {
if c >= 48 and c <= 57 { return (cast(i64) c) - 48; } // '0'..'9'
if c >= 97 and c <= 102 { return (cast(i64) c) - 97 + 10; } // 'a'..'f'
if c >= 65 and c <= 70 { return (cast(i64) c) - 65 + 10; } // 'A'..'F'
@@ -450,7 +450,7 @@ Parser :: struct {
// Read 4 hex digits at `i` (which must lie within [.., end)); returns
// the 16-bit value. Fewer than 4 digits before `end` is a BadEscape.
read_hex4 :: (self: *Parser, i: i64, end: i64) -> i64 !JsonParseError {
read_hex4 :: (self: *Parser, i: i64, end: i64) -> (i64, !JsonParseError) {
if i + 4 > end { raise error.BadEscape; }
v := 0;
k := 0;
@@ -464,7 +464,7 @@ Parser :: struct {
// Decode the escaped string body in [start, end) into `out`, returning
// the decoded byte length. Pass 1 (in parse_string) guarantees there is
// no dangling backslash, so the byte after every `\` is in range.
decode_into :: (self: *Parser, start: i64, end: i64, out: [*]u8) -> i64 !JsonParseError {
decode_into :: (self: *Parser, start: i64, end: i64, out: [*]u8) -> (i64, !JsonParseError) {
di := 0;
i := start;
while i < end {
@@ -511,7 +511,7 @@ Parser :: struct {
// a zero-copy VIEW into `src` when the body has no escapes; otherwise
// decodes into an `alloc`-ed buffer (bounded by the raw span). `pos`
// ends just past the closing quote.
parse_string :: (self: *Parser) -> string !JsonParseError {
parse_string :: (self: *Parser) -> (string, !JsonParseError) {
self.pos += 1; // consume opening quote
start := self.pos;
has_escape := false;
@@ -547,7 +547,7 @@ Parser :: struct {
// Parse an i64 integer (optional '-', then digits). Rejects leading
// zeros, a fraction/exponent tail, and any value outside i64 — all
// `BadNumber`. Accumulates in NEGATIVE space so i64 MIN parses exactly.
parse_number :: (self: *Parser) -> i64 !JsonParseError {
parse_number :: (self: *Parser) -> (i64, !JsonParseError) {
// i64 bounds, built positionally because |MIN| is not a
// representable positive i64 literal. `min_div10` is `MIN / 10`
// truncated toward zero (remainder -8) — the digit loop's overflow
@@ -585,7 +585,7 @@ Parser :: struct {
}
// Parse an array starting at '['. Builds an `Array` through `alloc`.
parse_array :: (self: *Parser) -> Value !JsonParseError {
parse_array :: (self: *Parser) -> (Value, !JsonParseError) {
self.pos += 1; // consume '['
arr : Array = .{};
self.skip_ws();
@@ -609,7 +609,7 @@ Parser :: struct {
// Parse an object starting at '{'. Keys must be strings; insertion
// order is preserved (duplicate keys are kept, never merged).
parse_object :: (self: *Parser) -> Value !JsonParseError {
parse_object :: (self: *Parser) -> (Value, !JsonParseError) {
self.pos += 1; // consume '{'
obj : Object = .{};
self.skip_ws();
@@ -640,7 +640,7 @@ Parser :: struct {
}
// Parse any single value (after skipping leading whitespace).
parse_value :: (self: *Parser) -> Value !JsonParseError {
parse_value :: (self: *Parser) -> (Value, !JsonParseError) {
self.skip_ws();
if self.pos >= self.src.len { raise error.UnexpectedEnd; }
c := self.src[self.pos];
@@ -659,7 +659,7 @@ Parser :: struct {
// `alloc` for composite nodes and decoded (escaped) strings. Un-escaped
// string values are VIEWS into `src` and are valid only while `src` lives.
// Trailing non-whitespace after the value raises `error.TrailingGarbage`.
parse :: (src: string, alloc: Allocator) -> Value !JsonParseError {
parse :: (src: string, alloc: Allocator) -> (Value, !JsonParseError) {
p := Parser.{ src = src, alloc = alloc };
v := try p.parse_value();
p.skip_ws();