feat: tuple syntax cutover — Tuple(...) type + .(...) value

Replace the bare-paren tuple grammar with explicit, position-unambiguous
forms, mirroring how structs work:

  type     `(A, B)`        -> `Tuple(A, B)`          (named keeps `:`)
  value    `(a, b)`        -> `.(a, b)`              (named uses `=`)
  typed    (new)           -> `Tuple(A, B).(a, b)`   (like `Point.{...}`)
  failable `-> (T, !)`     -> `-> T !`
           `-> (T1, T2, !)`-> `-> Tuple(T1, T2) !`   (channel outside Tuple)

Bare `(...)` is now grouping only, everywhere; a comma in bare parens is a
hard error with a migration hint. Grouping, function types `(A, B) -> R`,
param lists, lambdas, and match bindings are unaffected.

`Tuple(...)` is strictly a TYPE in every position (including `size_of` /
`type_info` args); a tuple VALUE comes only from `.(...)` (anonymous) or
`Tuple(...).(...)` (explicitly typed). A bare `Tuple(1, 2)` is a tuple
type with non-type elements -> rejected.

The ~110 tuple-bearing corpus files were migrated with a one-shot
AST-aware migrator (the `sx migrate` tool from the prior commit, removed
here). New examples: 0130 (new syntax), 0131 (typed construction), 1060
(named-tuple failable return). 1116 golden updated for the new hint text.
This commit is contained in:
agra
2026-06-25 17:53:57 +03:00
parent c882c6c63e
commit 989e18b760
124 changed files with 941 additions and 1236 deletions

View File

@@ -10,7 +10,7 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 10; // success → {n*10, 0}

View File

@@ -10,14 +10,14 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;
}
// value-carrying `try` in a value-carrying caller — propagates {undef, tag}.
inc :: (n: i32) -> (i32, !E) {
inc :: (n: i32) -> i32 !E {
v := try parse(n);
return v + 1;
}

View File

@@ -8,7 +8,7 @@
E :: error { Bad }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
return n;
}

View File

@@ -9,7 +9,7 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;

View File

@@ -12,37 +12,37 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, i32, !E) {
parse :: (n: i32) -> Tuple(i32, i32) !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return (n * 2, n + 1); // success → {n*2, n+1, 0}
return .(n * 2, n + 1); // success → {n*2, n+1, 0}
}
// Multi-value `try` in a multi-value caller — propagates {undef, undef, tag}.
inc :: (n: i32) -> (i32, i32, !E) {
inc :: (n: i32) -> Tuple(i32, i32) !E {
v, b := try parse(n);
return (v + 1, b + 1);
return .(v + 1, b + 1);
}
// Multi-value `catch`, bare-expression tuple fallback (absorbs the failure).
safe :: (n: i32) -> i32 {
v, b := parse(n) catch (e) (40, 50);
v, b := parse(n) catch (e) .(40, 50);
return v + b;
}
// Multi-value `catch` match-body — per-tag dispatch, each arm a value-tuple.
classify :: (n: i32) -> i32 {
v, b := parse(n) catch (e) == {
case .Bad: (1, 1);
case .Empty: (2, 2);
else: (9, 9);
case .Bad: .(1, 1);
case .Empty: .(2, 2);
else: .(9, 9);
};
return v + b;
}
// Multi-value `or (tuple)` value-terminator (absorbs the failure).
ortest :: (n: i32) -> i32 {
v, b := parse(n) or (7, 8);
v, b := parse(n) or .(7, 8);
return v + b;
}

View File

@@ -13,12 +13,12 @@
E :: error { Bad, Empty }
pair :: (n: i32) -> (i32, i32, !E) {
pair :: (n: i32) -> Tuple(i32, i32) !E {
if n < 0 { raise error.Bad; }
return (n, n + 1);
return .(n, n + 1);
}
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
return n * 2;
}

View File

@@ -8,7 +8,7 @@
E :: error { BadDigit, Empty, Overflow }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.BadDigit; }
if n == 0 { raise error.Empty; }
return n * 2;

View File

@@ -14,7 +14,7 @@
ParseErr :: error { Empty, BadDigit };
inner :: (n: i32) -> (i32, !ParseErr) {
inner :: (n: i32) -> i32 !ParseErr {
if n == 0 { raise error.Empty; } // pushes a frame
if n < 0 { raise error.BadDigit; }
return n * 2;

View File

@@ -9,13 +9,13 @@
ParseErr :: error { Empty, BadDigit };
inner :: (n: i32) -> (i32, !ParseErr) {
inner :: (n: i32) -> i32 !ParseErr {
if n == 0 { raise error.Empty; }
if n < 0 { raise error.BadDigit; }
return n * 2;
}
main :: () -> (i32, !ParseErr) {
main :: () -> i32 !ParseErr {
v := try inner(32); // succeeds → v = 64
print("v = {}\n", v);
return v; // success → exit code 64

View File

@@ -12,7 +12,7 @@
E :: error { A, B };
fa :: (n: i32) -> (i32, !E) {
fa :: (n: i32) -> i32 !E {
if n == 0 { raise error.A; }
if n < 0 { raise error.B; }
return n;
@@ -23,7 +23,7 @@ fv :: (n: i32) -> !E { // void (pure) failable
return;
}
main :: () -> (i32, !E) {
main :: () -> i32 !E {
onfail print("onfail fired (BUG)\n"); // must NOT fire — every chain below absorbs
r : i32 = 0;

View File

@@ -10,12 +10,12 @@
E :: error { A };
fa :: (n: i32) -> (i32, !E) {
fa :: (n: i32) -> i32 !E {
if n == 0 { raise error.A; }
return n;
}
main :: () -> (i32, !E) {
main :: () -> i32 !E {
v := try fa(0) or try fa(0) or try fa(0); // all fail → propagate to main
return v;
}

View File

@@ -9,7 +9,7 @@
SmokeErr :: error { Empty, BadDigit, Overflow }
// value-carrying, named set
sm_parse :: (n: i32) -> (i32, !SmokeErr) {
sm_parse :: (n: i32) -> i32 !SmokeErr {
if n < 0 { raise error.BadDigit; }
if n == 0 { raise error.Empty; }
if n > 99 { raise error.Overflow; }
@@ -23,10 +23,10 @@ sm_check :: (ok: bool) -> ! {
}
// multi-value, inferred set: `try` propagates; SCC absorbs SmokeErr
sm_pair :: (a: i32, b: i32) -> (i32, i32, !) {
sm_pair :: (a: i32, b: i32) -> Tuple(i32, i32) ! {
x := try sm_parse(a);
y := try sm_parse(b);
return (x, y);
return .(x, y);
}
// catch with a diverging block body
@@ -38,7 +38,7 @@ sm_or_default :: (n: i32) -> i32 {
}
// onfail + defer interleave: cleanup runs only on the error path
sm_acquire :: (fail: bool) -> (i32, !) {
sm_acquire :: (fail: bool) -> i32 ! {
defer print(" defer A\n");
onfail print(" onfail B\n");
if fail { raise error.Acquire; }
@@ -46,7 +46,7 @@ sm_acquire :: (fail: bool) -> (i32, !) {
}
// or-chain: try a, fall to try b; propagate if both fail
sm_first :: (a: i32, b: i32) -> (i32, !) {
sm_first :: (a: i32, b: i32) -> i32 ! {
v := try sm_parse(a) or try sm_parse(b);
return v;
}
@@ -54,18 +54,18 @@ sm_first :: (a: i32, b: i32) -> (i32, !) {
// --- Composition (ERR E5.1): failable closures, widening, generics ---
// Closure(...) param, try-propagated (the env is carried)
sm_run :: (cb: Closure(i32) -> (i32, !SmokeErr), n: i32) -> (i32, !SmokeErr) {
sm_run :: (cb: Closure(i32) -> i32 !SmokeErr, n: i32) -> i32 !SmokeErr {
return try cb(n);
}
// bare fn-type param: a NON-failable closure literal widens into the failable
// slot (the ∅-widening adapter wraps `{value, 0}`)
sm_widen :: (cb: (i32) -> (i32, !SmokeErr), n: i32) -> i32 {
sm_widen :: (cb: (i32) -> i32 !SmokeErr, n: i32) -> i32 {
return cb(n) catch (e) -1;
}
// generic ($T) value-carrying failable composition, monomorphized per call
sm_wrap :: ($T: Type, f: Closure() -> (T, !SmokeErr)) -> (T, !SmokeErr) {
sm_wrap :: ($T: Type, f: Closure() -> T !SmokeErr) -> T !SmokeErr {
return try f();
}
@@ -105,9 +105,9 @@ main :: () {
if !gerr { print("or-chain: {}\n", g); }
// multi-value failable consumed by catch (tuple body)
p, q := sm_pair(0, 3) catch (e) (0, 0);
p, q := sm_pair(0, 3) catch (e) .(0, 0);
print("pair-catch: {} {}\n", p, q);
p2, q2 := sm_pair(4, 5) catch (e) (0, 0);
p2, q2 := sm_pair(4, 5) catch (e) .(0, 0);
print("pair-ok: {} {}\n", p2, q2);
// pure failable: absorb with no-binding catch
@@ -120,15 +120,15 @@ main :: () {
iv, ierr := sm_acquire(false);
// composition: inline failable closure literal through a Closure(...) param
cl := sm_run(closure((x: i32) -> (i32, !SmokeErr) { if x < 0 { raise error.BadDigit; } return x * 2; }), 6) catch (e) -1;
cl := sm_run(closure((x: i32) -> i32 !SmokeErr { if x < 0 { raise error.BadDigit; } return x * 2; }), 6) catch (e) -1;
print("closure-run: {}\n", cl); // 12
print("closure-run-err: {}\n", sm_run(closure((x: i32) -> (i32, !SmokeErr) { raise error.Empty; }), 1) catch (e) -9); // -9
print("closure-run-err: {}\n", sm_run(closure((x: i32) -> i32 !SmokeErr { raise error.Empty; }), 1) catch (e) -9); // -9
// non-failable closure literal widened into the failable bare slot
print("widen: {}\n", sm_widen(closure((x: i32) -> i32 => x + 1), 9)); // 10
// generic failable composition (monomorphized at i32)
print("wrap: {}\n", sm_wrap(i32, closure(() -> (i32, !SmokeErr) { return 42; })) catch (e) 0); // 42
print("wrap: {}\n", sm_wrap(i32, closure(() -> i32 !SmokeErr { return 42; })) catch (e) 0); // 42
print("errors ok\n");
}

View File

@@ -7,7 +7,7 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;

View File

@@ -7,7 +7,7 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;

View File

@@ -10,12 +10,12 @@
E :: error { Neg }
runwith :: (cb: Closure(i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; }
runwith :: (cb: Closure(i64) -> i64 !E, n: i64) -> i64 { return cb(n) catch (e) -1; }
main :: () -> i32 {
// block-body and arrow-body failable closures, called directly
m := closure((x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; });
n := closure((x: i64) -> (i64, !E) => x + 1);
m := closure((x: i64) -> i64 !E { if x < 0 { raise error.Neg; } return x * 2; });
n := closure((x: i64) -> i64 !E => x + 1);
print("{} {} {} {}\n", m(5) catch (e) 0, m(-1) catch (e) 99, m(-1) or 7, n(40) catch (e) 0); // 10 99 7 41
// failable closure passed as a Closure(...) parameter

View File

@@ -12,21 +12,21 @@
E :: error { Neg }
bare :: (cb: (i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; }
chain :: (cb: Closure(i64) -> (i64, !E), n: i64) -> (i64, !E) { return try cb(n); }
bare :: (cb: (i64) -> i64 !E, n: i64) -> i64 { return cb(n) catch (e) -1; }
chain :: (cb: Closure(i64) -> i64 !E, n: i64) -> i64 !E { return try cb(n); }
dbl :: (x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; }
dbl :: (x: i64) -> i64 !E { if x < 0 { raise error.Neg; } return x * 2; }
main :: () -> i32 {
// failable closure literal through a bare fn-type param (matching ABI)
print("bare ok={} err={}\n",
bare(closure((x: i64) -> (i64, !E) { if x < 0 { raise error.Neg; } return x * 2; }), 5),
bare(closure((x: i64) -> (i64, !E) => x * 2), -1)); // ok=10; err: arrow never raises → cb(-1) = -2
bare(closure((x: i64) -> i64 !E { if x < 0 { raise error.Neg; } return x * 2; }), 5),
bare(closure((x: i64) -> i64 !E => x * 2), -1)); // ok=10; err: arrow never raises → cb(-1) = -2
// Closure(...) param, try-propagated, then caught at the call site
print("chain ok={} err={}\n",
chain(closure((x: i64) -> (i64, !E) => x + 6), 4) catch (e) 0, // 10
chain(closure((x: i64) -> (i64, !E) { raise error.Neg; }), 1) catch (e) 0); // 0
chain(closure((x: i64) -> i64 !E => x + 6), 4) catch (e) 0, // 10
chain(closure((x: i64) -> i64 !E { raise error.Neg; }), 1) catch (e) 0); // 0
// NON-failable closure literal widened into the failable bare slot
print("widen={}\n", bare(closure((x: i64) -> i64 => x + 1), 9)); // 10

View File

@@ -10,7 +10,7 @@
All :: error { Negative, Other }
// `h` is a bare-`!` Closure slot; the caller declares the union as `!All`.
dispatch :: (h: Closure(i32) -> (i32, !), x: i32) -> (i32, !All) {
dispatch :: (h: Closure(i32) -> i32 !, x: i32) -> i32 !All {
return try h(x);
}
@@ -19,9 +19,9 @@ main :: () -> i32 {
push Context.{ allocator = xx gpa } {
// Two literals of the SAME shape raising DIFFERENT tags both feed the
// one shared `Closure(i32)->(i32,!)` union node.
handlers : List(Closure(i32) -> (i32, !)) = .{};
handlers.append(closure((x: i32) -> (i32, !) { if x < 0 { raise error.Negative; } return x * 2; }));
handlers.append(closure((x: i32) -> (i32, !) { if x == 0 { raise error.Other; } return x + 100; }));
handlers : List(Closure(i32) -> i32 !) = .{};
handlers.append(closure((x: i32) -> i32 ! { if x < 0 { raise error.Negative; } return x * 2; }));
handlers.append(closure((x: i32) -> i32 ! { if x == 0 { raise error.Other; } return x + 100; }));
// success paths
print("ok0={}\n", dispatch(handlers.items[0], 5) catch (e) 0); // 10

View File

@@ -9,16 +9,16 @@
Small :: error { Unrelated }
reject :: (h: Closure(i32) -> (i32, !), x: i32) -> (i32, !Small) {
reject :: (h: Closure(i32) -> i32 !, x: i32) -> i32 !Small {
return try h(x); // Negative, Other ∉ Small → two diagnostics
}
main :: () -> i32 {
gpa := GPA.init();
push Context.{ allocator = xx gpa } {
handlers : List(Closure(i32) -> (i32, !)) = .{};
handlers.append(closure((x: i32) -> (i32, !) { if x < 0 { raise error.Negative; } return x; }));
handlers.append(closure((x: i32) -> (i32, !) { if x == 0 { raise error.Other; } return x; }));
handlers : List(Closure(i32) -> i32 !) = .{};
handlers.append(closure((x: i32) -> i32 ! { if x < 0 { raise error.Negative; } return x; }));
handlers.append(closure((x: i32) -> i32 ! { if x == 0 { raise error.Other; } return x; }));
print("r={}\n", reject(handlers.items[0], 5) catch (e) 0);
}
return 0;

View File

@@ -9,7 +9,7 @@
E :: error { Neg }
take :: (cb: Closure(i32) -> (i32, !E), x: i32) -> i32 { return cb(x) catch (e) -1; }
take :: (cb: Closure(i32) -> i32 !E, x: i32) -> i32 { return cb(x) catch (e) -1; }
main :: () -> i32 {
// `-> i32` (non-failable) but the body raises → lambda-specific hint:

View File

@@ -9,21 +9,21 @@
E :: error { Bad }
wrap :: ($T: Type, f: Closure() -> (T, !E)) -> (T, !E) { return try f(); }
wrap :: ($T: Type, f: Closure() -> T !E) -> T !E { return try f(); }
main :: () -> i32 {
// success, consumed by catch
print("catch={}\n", wrap(i32, closure(() -> (i32, !E) { return 7; })) catch (e) -1); // 7
print("catch={}\n", wrap(i32, closure(() -> i32 !E { return 7; })) catch (e) -1); // 7
// success, consumed by destructure (binds value + error slot); the value
// slot is read only under an `if !err` guard (ERR E1.8 path-sensitivity)
r, err := wrap(i32, closure(() -> (i32, !E) { return 9; }));
r, err := wrap(i32, closure(() -> i32 !E { return 9; }));
if !err { print("destr={} ok=true\n", r); } // destr=9 ok=true
// failure path: the raised tag propagates through the generic `try`
print("fail={}\n", wrap(i32, closure(() -> (i32, !E) { raise error.Bad; }) ) catch (e) -1); // -1
print("fail={}\n", wrap(i32, closure(() -> i32 !E { raise error.Bad; }) ) catch (e) -1); // -1
// a second monomorphization at a different T
print("u8={}\n", wrap(u8, closure(() -> (u8, !E) { return 200; })) catch (e) 0); // 200
print("u8={}\n", wrap(u8, closure(() -> u8 !E { return 200; })) catch (e) 0); // 200
return 0;
}

View File

@@ -13,7 +13,7 @@
E :: error { Z }
bare :: (cb: (i64) -> i64, n: i64) -> i64 { return cb(n); }
baref :: (cb: (i64) -> (i64, !E), n: i64) -> i64 { return cb(n) catch (e) -1; }
baref :: (cb: (i64) -> i64 !E, n: i64) -> i64 { return cb(n) catch (e) -1; }
main :: () -> i32 {
inc := closure((x: i64) -> i64 => x + 1); // capture-free closure var

View File

@@ -15,7 +15,7 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 10;
@@ -29,7 +29,7 @@ guarded :: (n: i32) -> i32 {
}
// `if err { raise }` in a failable function: same fall-through proof.
relay :: (n: i32) -> (i32, !E) {
relay :: (n: i32) -> i32 !E {
v, err := parse(n);
if err { raise err; }
return v + 1; // err proven absent here

View File

@@ -11,7 +11,7 @@
E :: error { Bad }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
return n * 10;
}

View File

@@ -9,7 +9,7 @@
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
recover :: () -> (i32, !E) { raise error.Bad; }
recover :: () -> i32 !E { raise error.Bad; }
work :: (n: i32) -> !E {
defer print("defer: always\n"); // plain cleanup

View File

@@ -9,7 +9,7 @@
E :: error { Bad }
probe :: () -> (i32, !E) { return 21; }
probe :: () -> i32 !E { return 21; }
failing :: () -> !E { raise error.Bad; }
run :: () {

View File

@@ -24,7 +24,7 @@
E :: error { Bad }
failing :: () -> !E { raise error.Bad; }
recover :: () -> (i32, !E) { return 21; }
recover :: () -> i32 !E { return 21; }
work :: () {
defer {

View File

@@ -12,7 +12,7 @@
E :: error { Bad }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
return n * 10;
}

View File

@@ -8,7 +8,7 @@
E :: error { Bad, Empty }
parse :: (n: i32) -> (i32, !E) {
parse :: (n: i32) -> i32 !E {
if n < 0 { raise error.Bad; }
if n == 0 { raise error.Empty; }
return n * 2;

View File

@@ -16,7 +16,7 @@
Color :: enum { red; green; blue; }
E :: error { Nope }
pick :: (s: string) -> (Color, !E) {
pick :: (s: string) -> Color !E {
if s == "red" { return .red; }
if s == "blue" { return .blue; } // non-zero ordinal (2)
raise error.Nope;

View File

@@ -22,13 +22,13 @@ Color :: enum { red; green; blue; }
E :: error { Nope }
// F1: bare-value success path AND explicit-tuple error path in one function.
classify :: (s: string) -> (Color, !E) {
classify :: (s: string) -> Color !E {
if s == "ok" { return .blue; } // bare value → {2, 0}
return (.red, error.Nope); // explicit full tuple → {0, 1}
return .(.red, error.Nope); // explicit full tuple → {0, 1}
}
// F2: comptime parameter forces inline lowering of the body.
ct_pick :: ($n: i32, s: string) -> (Color, !E) {
ct_pick :: ($n: i32, s: string) -> Color !E {
if s == "red" { return .red; } // bare value, inline path → {0, 0}
if s == "blue" { return .blue; } // bare value, inline path → {2, 0}
raise error.Nope; // inline error path → {undef, 1}

View File

@@ -7,7 +7,7 @@
E :: error { Boom }
f :: (fail: bool) -> (i64, !E) {
f :: (fail: bool) -> i64 !E {
if fail { raise error.Boom; }
return 42;
}

View File

@@ -5,4 +5,4 @@ LE :: error { Bad }
Box :: struct ($R: Type) { v: R; }
// Returns `($R, !LE)` — a value-failable. `$R` is inferred from the arg.
get :: ufcs (b: *Box($R)) -> ($R, !LE) { return b.v; }
get :: ufcs (b: *Box($R)) -> $R !LE { return b.v; }

View File

@@ -0,0 +1,26 @@
// A failable function returning a NAMED tuple value `-> Tuple(x: A, y: B) !E`
// flattens its value fields into the result tuple (`{ x: A, y: B, err }`),
// keeping the `.x`/`.y` names addressable on both the success value and the
// `or` fallback. Exercises the success path, a `raise` path, and an
// `or .(x=.., y=..)` terminator.
//
// Regression (issue 0179-adjacent named-tuple-failable miscompile): a named
// failable tuple used to WRAP as `{ {A,B}, err }` while the value-return
// lowering inserted the value slots FLAT, producing invalid LLVM
// (`Invalid InsertValueInst operands`).
#import "modules/std.sx";
E :: error { Bad }
two :: (n: i64) -> Tuple(x: i64, y: i64) !E {
if n < 0 { raise error.Bad; }
return .(x = n, y = n + 1);
}
main :: () {
ok := two(5) or .(x = 0, y = 0);
print("{} {}\n", ok.x, ok.y);
bad := two(-1) or .(x = 0, y = 0);
print("{} {}\n", bad.x, bad.y);
}

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,2 @@
5 6
0 0