diff --git a/lib/ww/parse/stmt.ww b/lib/ww/parse/stmt.ww index b62097b7..fd854fcc 100644 --- a/lib/ww/parse/stmt.ww +++ b/lib/ww/parse/stmt.ww @@ -176,6 +176,63 @@ fn parsefor(p: *parser) *node = { return n; }; +fn parseswitch(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `switch` + expecttok(p, tkind.TK_LPAREN, "expected '(' after switch"); + let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc); + n.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression"); + expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body"); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_CASE) { + let cpf: str = p.curfile; + let cpl: i32 = p.curline; + let cpc: i32 = p.curcol; + advance(p); // past `case` + let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc); + let eh: *node = nil; + let et: *node = nil; + if (p.curkind != tkind.TK_COLON) { + p.nocast = 1; + for (true) { + let e: *node = parseexpr(p); + if (eh == nil) { eh = e; } + else { et.next = e; }; + et = e; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + p.nocast = 0; + }; + cs.list = eh; + expecttok(p, tkind.TK_COLON, "expected ':' after case label"); + let bh: *node = nil; + let bt: *node = nil; + for (p.curkind != tkind.TK_CASE) { + if (p.curkind == tkind.TK_RBRACE) { break; }; + if (p.curkind == tkind.TK_EOF) { break; }; + let s: *node = parsestmt(p); + if (s != nil) { + if (bh == nil) { bh = s; } + else { bt.next = s; }; + bt = s; + }; + }; + let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc); + blk.list = bh; + cs.body = blk; + if (head == nil) { head = cs; } + else { tail.next = cs; }; + tail = cs; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch"); + n.list = head; + return n; +}; + fn parsestmt(p: *parser) *node = { let pf: str = p.curfile; let pl: i32 = p.curline; @@ -202,6 +259,11 @@ fn parsestmt(p: *parser) *node = { expecttok(p, tkind.TK_SEMI, "expected ';' after for"); return n; }; + if (p.curkind == tkind.TK_SWITCH) { + let n: *node = parseswitch(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after switch"); + return n; + }; if (p.curkind == tkind.TK_RETURN) { advance(p); let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index aa0e7268..f49679c6 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -2743,6 +2743,63 @@ fn parsefor(p: *parser) *node = { return n; }; +fn parseswitch(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `switch` + expecttok(p, tkind.TK_LPAREN, "expected '(' after switch"); + let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc); + n.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression"); + expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body"); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_CASE) { + let cpf: str = p.curfile; + let cpl: i32 = p.curline; + let cpc: i32 = p.curcol; + advance(p); // past `case` + let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc); + let eh: *node = nil; + let et: *node = nil; + if (p.curkind != tkind.TK_COLON) { + p.nocast = 1; + for (true) { + let e: *node = parseexpr(p); + if (eh == nil) { eh = e; } + else { et.next = e; }; + et = e; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + p.nocast = 0; + }; + cs.list = eh; + expecttok(p, tkind.TK_COLON, "expected ':' after case label"); + let bh: *node = nil; + let bt: *node = nil; + for (p.curkind != tkind.TK_CASE) { + if (p.curkind == tkind.TK_RBRACE) { break; }; + if (p.curkind == tkind.TK_EOF) { break; }; + let s: *node = parsestmt(p); + if (s != nil) { + if (bh == nil) { bh = s; } + else { bt.next = s; }; + bt = s; + }; + }; + let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc); + blk.list = bh; + cs.body = blk; + if (head == nil) { head = cs; } + else { tail.next = cs; }; + tail = cs; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch"); + n.list = head; + return n; +}; + fn parsestmt(p: *parser) *node = { let pf: str = p.curfile; let pl: i32 = p.curline; @@ -2769,6 +2826,11 @@ fn parsestmt(p: *parser) *node = { expecttok(p, tkind.TK_SEMI, "expected ';' after for"); return n; }; + if (p.curkind == tkind.TK_SWITCH) { + let n: *node = parseswitch(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after switch"); + return n; + }; if (p.curkind == tkind.TK_RETURN) { advance(p); let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc); @@ -8195,6 +8257,8 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_FOR) { cgfor(c, n); return; }; + if (k == nkind.N_SWITCH) { cgswitch(c, n); return; }; + if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; }; if (k == nkind.N_MLET) { cgmlet(c, n); return; }; @@ -8877,6 +8941,65 @@ fn cgmlet(c: *cgen, n: *node) void = { return; }; +// cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to +// a chain of compares against the scrutinee. Scrutinee lands in a +// fresh 8B local slot so case bodies can spill SP without losing it. +// Cases are tried top-to-bottom; the `case:` arm with no exprs is the +// default and runs after all named arms fail. Mirrors cmd/w6c/cgen.c +// N_SWITCH: same labelseq consumption order so labels match byte-for- +// byte. +fn cgswitch(c: *cgen, n: *node) void = { + let swname: str = mkscratchname(c, "sw"); + let sloff: i32 = localalloc(c, swname, 8, nil); + + if (n.lhs != nil) { cgexpr(c, n.lhs); }; + emitline("\tMOVQ\tAX, "); + emitoff(sloff: i64); + emitline("(BP)\n"); + + let endl: str = mklabel(c, "swend"); + let defcase: *node = nil; + + let cs: *node = n.list; + for (cs != nil) { + if (cs.list == nil) { + defcase = cs; + cs = cs.next; + continue; + }; + let body: str = mklabel(c, "swcase"); + let nxt: str = mklabel(c, "swnext"); + let e: *node = cs.list; + for (e != nil) { + cgexpr(c, e); + emitline("\tMOVQ\t"); + emitoff(sloff: i64); + emitline("(BP), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJE\t"); + emitline(body); + emitline("\n"); + e = e.next; + }; + emitline("\tJMP\t"); + emitline(nxt); + emitline("\n"); + emitlabel(body); + if (cs.body != nil) { cgstmt(c, cs.body); }; + emitline("\tJMP\t"); + emitline(endl); + emitline("\n"); + emitlabel(nxt); + cs = cs.next; + }; + if (defcase != nil) { + if (defcase.body != nil) { cgstmt(c, defcase.body); }; + }; + emitlabel(endl); + c.lastwasreturn = 0; + return; +}; + fn cgbreak(c: *cgen, n: *node) void = { if (c.looptop > 0) { let lbl: str = c.loopendbuf[c.looptop - 1]; @@ -8987,6 +9110,11 @@ fn scanlocals(c: *cgen, n: *node) i32 = { bidx += 1; }; }; + // `switch` allocates an 8B scratch slot for the scrutinee so case + // bodies can spill through SP without losing it. The slot is named + // ".sw_" at cgen time — unique per switch — so it must + // not dedup. Count it here so the frame SUBQ matches. + if (n.kind == nkind.N_SWITCH) { total += 8; }; // Match-arm binding (`case let v: T => ...`) gets a slot too. // Crucially we do NOT dedup these against c.locals: C cgen // handles a match as an expression with a by-value locals copy, @@ -9750,6 +9878,37 @@ fn emitlabel(s: str) void = { emitline(":\n"); }; +// mkscratchname — fresh local-slot name "._". Used for +// compiler-synthesised slots (switch scrutinee, forrange index/len) +// that need to be unique per use site but are never referenced by user +// code. Increments labelseq so the same source position lines up with +// C cgen's labelseq stream. +fn mkscratchname(c: *cgen, base: str) str = { + let buf: [128]u8; + let i: i32 = 0; + buf[i] = 46u8; i += 1; // '.' + let j: i32 = 0; + for (j < base.len) { + buf[i] = base[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + let n: i32 = strconv.i64tos(buf[i:128], c.labelseq: i64); + c.labelseq += 1; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { + p[k] = buf[k]; + k += 1; + }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + // ---- string interning ------------------------------------------------ // // streq is provided by sym.ww and reused here. diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 44b0d4c4..1ea502b6 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -524,6 +524,37 @@ fn emitlabel(s: str) void = { emitline(":\n"); }; +// mkscratchname — fresh local-slot name "._". Used for +// compiler-synthesised slots (switch scrutinee, forrange index/len) +// that need to be unique per use site but are never referenced by user +// code. Increments labelseq so the same source position lines up with +// C cgen's labelseq stream. +fn mkscratchname(c: *cgen, base: str) str = { + let buf: [128]u8; + let i: i32 = 0; + buf[i] = 46u8; i += 1; // '.' + let j: i32 = 0; + for (j < base.len) { + buf[i] = base[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + let n: i32 = strconv.i64tos(buf[i:128], c.labelseq: i64); + c.labelseq += 1; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { + p[k] = buf[k]; + k += 1; + }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + // ---- string interning ------------------------------------------------ // // streq is provided by sym.ww and reused here. diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 0b395910..f1931f95 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -87,6 +87,11 @@ fn scanlocals(c: *cgen, n: *node) i32 = { bidx += 1; }; }; + // `switch` allocates an 8B scratch slot for the scrutinee so case + // bodies can spill through SP without losing it. The slot is named + // ".sw_" at cgen time — unique per switch — so it must + // not dedup. Count it here so the frame SUBQ matches. + if (n.kind == nkind.N_SWITCH) { total += 8; }; // Match-arm binding (`case let v: T => ...`) gets a slot too. // Crucially we do NOT dedup these against c.locals: C cgen // handles a match as an expression with a by-value locals copy, diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 8b20acd9..cbe19755 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -34,6 +34,8 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_FOR) { cgfor(c, n); return; }; + if (k == nkind.N_SWITCH) { cgswitch(c, n); return; }; + if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; }; if (k == nkind.N_MLET) { cgmlet(c, n); return; }; @@ -716,6 +718,65 @@ fn cgmlet(c: *cgen, n: *node) void = { return; }; +// cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to +// a chain of compares against the scrutinee. Scrutinee lands in a +// fresh 8B local slot so case bodies can spill SP without losing it. +// Cases are tried top-to-bottom; the `case:` arm with no exprs is the +// default and runs after all named arms fail. Mirrors cmd/w6c/cgen.c +// N_SWITCH: same labelseq consumption order so labels match byte-for- +// byte. +fn cgswitch(c: *cgen, n: *node) void = { + let swname: str = mkscratchname(c, "sw"); + let sloff: i32 = localalloc(c, swname, 8, nil); + + if (n.lhs != nil) { cgexpr(c, n.lhs); }; + emitline("\tMOVQ\tAX, "); + emitoff(sloff: i64); + emitline("(BP)\n"); + + let endl: str = mklabel(c, "swend"); + let defcase: *node = nil; + + let cs: *node = n.list; + for (cs != nil) { + if (cs.list == nil) { + defcase = cs; + cs = cs.next; + continue; + }; + let body: str = mklabel(c, "swcase"); + let nxt: str = mklabel(c, "swnext"); + let e: *node = cs.list; + for (e != nil) { + cgexpr(c, e); + emitline("\tMOVQ\t"); + emitoff(sloff: i64); + emitline("(BP), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJE\t"); + emitline(body); + emitline("\n"); + e = e.next; + }; + emitline("\tJMP\t"); + emitline(nxt); + emitline("\n"); + emitlabel(body); + if (cs.body != nil) { cgstmt(c, cs.body); }; + emitline("\tJMP\t"); + emitline(endl); + emitline("\n"); + emitlabel(nxt); + cs = cs.next; + }; + if (defcase != nil) { + if (defcase.body != nil) { cgstmt(c, defcase.body); }; + }; + emitlabel(endl); + c.lastwasreturn = 0; + return; +}; + fn cgbreak(c: *cgen, n: *node) void = { if (c.looptop > 0) { let lbl: str = c.loopendbuf[c.looptop - 1]; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 54da9bb3..ed16e1c0 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -2743,6 +2743,63 @@ fn parsefor(p: *parser) *node = { return n; }; +fn parseswitch(p: *parser) *node = { + let pf: str = p.curfile; + let pl: i32 = p.curline; + let pc: i32 = p.curcol; + advance(p); // past `switch` + expecttok(p, tkind.TK_LPAREN, "expected '(' after switch"); + let n: *node = newnode(p.a, nkind.N_SWITCH, pf, pl, pc); + n.lhs = parseexpr(p); + expecttok(p, tkind.TK_RPAREN, "expected ')' after switch expression"); + expecttok(p, tkind.TK_LBRACE, "expected '{' to open switch body"); + let head: *node = nil; + let tail: *node = nil; + for (p.curkind == tkind.TK_CASE) { + let cpf: str = p.curfile; + let cpl: i32 = p.curline; + let cpc: i32 = p.curcol; + advance(p); // past `case` + let cs: *node = newnode(p.a, nkind.N_CASE, cpf, cpl, cpc); + let eh: *node = nil; + let et: *node = nil; + if (p.curkind != tkind.TK_COLON) { + p.nocast = 1; + for (true) { + let e: *node = parseexpr(p); + if (eh == nil) { eh = e; } + else { et.next = e; }; + et = e; + if (!accepttok(p, tkind.TK_COMMA)) { break; }; + }; + p.nocast = 0; + }; + cs.list = eh; + expecttok(p, tkind.TK_COLON, "expected ':' after case label"); + let bh: *node = nil; + let bt: *node = nil; + for (p.curkind != tkind.TK_CASE) { + if (p.curkind == tkind.TK_RBRACE) { break; }; + if (p.curkind == tkind.TK_EOF) { break; }; + let s: *node = parsestmt(p); + if (s != nil) { + if (bh == nil) { bh = s; } + else { bt.next = s; }; + bt = s; + }; + }; + let blk: *node = newnode(p.a, nkind.N_BLOCK, cpf, cpl, cpc); + blk.list = bh; + cs.body = blk; + if (head == nil) { head = cs; } + else { tail.next = cs; }; + tail = cs; + }; + expecttok(p, tkind.TK_RBRACE, "expected '}' to close switch"); + n.list = head; + return n; +}; + fn parsestmt(p: *parser) *node = { let pf: str = p.curfile; let pl: i32 = p.curline; @@ -2769,6 +2826,11 @@ fn parsestmt(p: *parser) *node = { expecttok(p, tkind.TK_SEMI, "expected ';' after for"); return n; }; + if (p.curkind == tkind.TK_SWITCH) { + let n: *node = parseswitch(p); + expecttok(p, tkind.TK_SEMI, "expected ';' after switch"); + return n; + }; if (p.curkind == tkind.TK_RETURN) { advance(p); let n: *node = newnode(p.a, nkind.N_RETURN, pf, pl, pc); @@ -8195,6 +8257,8 @@ fn cgstmt(c: *cgen, n: *node) void = { if (k == nkind.N_FOR) { cgfor(c, n); return; }; + if (k == nkind.N_SWITCH) { cgswitch(c, n); return; }; + if (k == nkind.N_MASSIGN) { cgmassign(c, n); return; }; if (k == nkind.N_MLET) { cgmlet(c, n); return; }; @@ -8877,6 +8941,65 @@ fn cgmlet(c: *cgen, n: *node) void = { return; }; +// cgswitch — lower `switch (e) { case 1, 2: ...; case: default; }` to +// a chain of compares against the scrutinee. Scrutinee lands in a +// fresh 8B local slot so case bodies can spill SP without losing it. +// Cases are tried top-to-bottom; the `case:` arm with no exprs is the +// default and runs after all named arms fail. Mirrors cmd/w6c/cgen.c +// N_SWITCH: same labelseq consumption order so labels match byte-for- +// byte. +fn cgswitch(c: *cgen, n: *node) void = { + let swname: str = mkscratchname(c, "sw"); + let sloff: i32 = localalloc(c, swname, 8, nil); + + if (n.lhs != nil) { cgexpr(c, n.lhs); }; + emitline("\tMOVQ\tAX, "); + emitoff(sloff: i64); + emitline("(BP)\n"); + + let endl: str = mklabel(c, "swend"); + let defcase: *node = nil; + + let cs: *node = n.list; + for (cs != nil) { + if (cs.list == nil) { + defcase = cs; + cs = cs.next; + continue; + }; + let body: str = mklabel(c, "swcase"); + let nxt: str = mklabel(c, "swnext"); + let e: *node = cs.list; + for (e != nil) { + cgexpr(c, e); + emitline("\tMOVQ\t"); + emitoff(sloff: i64); + emitline("(BP), BX\n"); + emitline("\tCMPQ\tBX, AX\n"); + emitline("\tJE\t"); + emitline(body); + emitline("\n"); + e = e.next; + }; + emitline("\tJMP\t"); + emitline(nxt); + emitline("\n"); + emitlabel(body); + if (cs.body != nil) { cgstmt(c, cs.body); }; + emitline("\tJMP\t"); + emitline(endl); + emitline("\n"); + emitlabel(nxt); + cs = cs.next; + }; + if (defcase != nil) { + if (defcase.body != nil) { cgstmt(c, defcase.body); }; + }; + emitlabel(endl); + c.lastwasreturn = 0; + return; +}; + fn cgbreak(c: *cgen, n: *node) void = { if (c.looptop > 0) { let lbl: str = c.loopendbuf[c.looptop - 1]; @@ -8987,6 +9110,11 @@ fn scanlocals(c: *cgen, n: *node) i32 = { bidx += 1; }; }; + // `switch` allocates an 8B scratch slot for the scrutinee so case + // bodies can spill through SP without losing it. The slot is named + // ".sw_" at cgen time — unique per switch — so it must + // not dedup. Count it here so the frame SUBQ matches. + if (n.kind == nkind.N_SWITCH) { total += 8; }; // Match-arm binding (`case let v: T => ...`) gets a slot too. // Crucially we do NOT dedup these against c.locals: C cgen // handles a match as an expression with a by-value locals copy, @@ -9750,6 +9878,37 @@ fn emitlabel(s: str) void = { emitline(":\n"); }; +// mkscratchname — fresh local-slot name "._". Used for +// compiler-synthesised slots (switch scrutinee, forrange index/len) +// that need to be unique per use site but are never referenced by user +// code. Increments labelseq so the same source position lines up with +// C cgen's labelseq stream. +fn mkscratchname(c: *cgen, base: str) str = { + let buf: [128]u8; + let i: i32 = 0; + buf[i] = 46u8; i += 1; // '.' + let j: i32 = 0; + for (j < base.len) { + buf[i] = base[j]; + i += 1; j += 1; + }; + buf[i] = 95u8; i += 1; // '_' + let n: i32 = strconv.i64tos(buf[i:128], c.labelseq: i64); + c.labelseq += 1; + let total: i32 = i + n; + let p: *u8 = amalloc(c.a, (total: u64) + 1u64): *u8; + let k: i32 = 0; + for (k < total) { + p[k] = buf[k]; + k += 1; + }; + p[total] = 0u8; + let r: str; + r.ptr = p; + r.len = total; + return r; +}; + // ---- string interning ------------------------------------------------ // // streq is provided by sym.ww and reused here. diff --git a/test/wcc/990_selfhost.c b/test/wcc/990_selfhost.c index 28e5a712..6388cd34 100644 --- a/test/wcc/990_selfhost.c +++ b/test/wcc/990_selfhost.c @@ -345,6 +345,21 @@ probe_ww_compile(const char *bin) " dec(&c); dec(&c); dec(&c);\n" " return c.x;\n" "};", 2 }, + /* switch with multi-expr cases + default. Lowers to a chain + * of CMPQ + JE; the scrutinee lands in a fresh 8B local slot + * (".sw_N") so case bodies can spill SP without losing it. + * classify(2)=10, classify(7)=70, classify(99)=0, sum 80. */ + { "fn classify(x: i32) i32 = {\n" + " switch (x) {\n" + " case 1, 2, 3: return 10;\n" + " case 7: return 70;\n" + " case: return 0;\n" + " };\n" + " return -1;\n" + "};\n" + "fn main() i32 = {\n" + " return classify(2) + classify(7) + classify(99);\n" + "};", 80 }, /* Chained N_DOT: o.ptr_to_inner.val (read field through a * pointer-to-struct field). Pre-fix the cgen fell through * silently and returned the inner pointer instead of the diff --git a/test/wcc/994_w6c_ww.c b/test/wcc/994_w6c_ww.c index af962807..12d4efc7 100644 --- a/test/wcc/994_w6c_ww.c +++ b/test/wcc/994_w6c_ww.c @@ -149,6 +149,16 @@ main(void) " let m: mode = mode.RW;\n" " return m as i32;\n" "};" }, + { "switch", + "fn classify(x: i32) i32 = {\n" + " switch (x) {\n" + " case 1, 2, 3: return 10;\n" + " case 7: return 70;\n" + " case: return 0;\n" + " };\n" + " return -1;\n" + "};\n" + "fn main() i32 = { return classify(2); };" }, { NULL, NULL }, };