1 module graphql.lexer;
2 
3 import std.experimental.logger;
4 import std.format : format;
5 import std.typecons :Flag;
6 import std.stdio;
7 
8 import graphql.tokenmodule;
9 
10 alias QueryParser = Flag!"QueryParser";
11 
12 struct Lexer {
13 	const QueryParser qp;
14 	string input;
15 	size_t stringPos;
16 
17 	size_t line;
18 	size_t column;
19 
20 	Token cur;
21 
22 	this(string input, QueryParser qp = QueryParser.yes) @safe {
23 		this.input = input;
24 		this.stringPos = 0;
25 		this.line = 1;
26 		this.column = 1;
27 		this.qp = qp;
28 		this.buildToken();
29 	}
30 
31 	bool isNotQueryParser() @safe const {
32 		return this.qp == QueryParser.no;
33 	}
34 
35 	private bool isTokenStop() const @safe {
36 		return this.stringPos >= this.input.length
37 			|| this.isTokenStop(this.input[this.stringPos]);
38 	}
39 
40 	private bool isTokenStop(const(char) c) const @safe {
41 		return
42 			c == ' ' || c == '\t' || c == '\n' || c == ';' || c == '('
43 			|| c == ')' || c == '{' || c == '}' || c == '!' || c == '='
44 			|| c == '|' || c == '*' || c == '/' || c == '[' || c == ':'
45 			|| c == ']' || c == ',' || c == '@' || c == '#' || c == '$';
46 	}
47 
48 	private void eatComment() @safe {
49 		if(this.stringPos < this.input.length &&
50 				this.input[this.stringPos] == '#')
51 		{
52 			while(this.stringPos < this.input.length &&
53 				this.input[this.stringPos] != '\n')
54 			{
55 				++this.stringPos;
56 			}
57 			++this.stringPos;
58 			++this.line;
59 		}
60 	}
61 
62 	private void eatWhitespace() @safe {
63 		import std.ascii : isWhite;
64 		while(this.stringPos < this.input.length) {
65 			this.eatComment();
66 			if(this.input[this.stringPos] == ' ') {
67 				++this.column;
68 			} else if(this.input[this.stringPos] == '\t') {
69 				++this.column;
70 			} else if(this.input[this.stringPos] == '\n') {
71 				this.column = 1;
72 				++this.line;
73 			} else {
74 				break;
75 			}
76 			++this.stringPos;
77 		}
78 	}
79 
80 	private void buildToken() @safe {
81 		import std.ascii : isAlphaNum;
82 		this.eatWhitespace();
83 
84 		if(this.stringPos >= this.input.length) {
85 			this.cur = Token(TokenType.undefined);
86 			return;
87 		}
88 
89 		if(this.input[this.stringPos] == ')') {
90 			this.cur = Token(TokenType.rparen, this.line, this.column);
91 			++this.column;
92 			++this.stringPos;
93 		} else if(this.input[this.stringPos] == '(') {
94 			this.cur = Token(TokenType.lparen, this.line, this.column);
95 			++this.column;
96 			++this.stringPos;
97 		} else if(this.input[this.stringPos] == ']') {
98 			this.cur = Token(TokenType.rbrack, this.line, this.column);
99 			++this.column;
100 			++this.stringPos;
101 		} else if(this.input[this.stringPos] == '[') {
102 			this.cur = Token(TokenType.lbrack, this.line, this.column);
103 			++this.column;
104 			++this.stringPos;
105 		} else if(this.input[this.stringPos] == '}') {
106 			this.cur = Token(TokenType.rcurly, this.line, this.column);
107 			++this.column;
108 			++this.stringPos;
109 		} else if(this.input[this.stringPos] == '$') {
110 			this.cur = Token(TokenType.dollar, this.line, this.column);
111 			++this.column;
112 			++this.stringPos;
113 		} else if(this.input[this.stringPos] == '!') {
114 			this.cur = Token(TokenType.exclamation, this.line, this.column);
115 			++this.column;
116 			++this.stringPos;
117 		} else if(this.input[this.stringPos] == '{') {
118 			this.cur = Token(TokenType.lcurly, this.line, this.column);
119 			++this.column;
120 			++this.stringPos;
121 		} else if(this.input[this.stringPos] == '|') {
122 			this.cur = Token(TokenType.pipe, this.line, this.column);
123 			++this.column;
124 			++this.stringPos;
125 		} else if(this.input[this.stringPos] == '@') {
126 			this.cur = Token(TokenType.at, this.line, this.column);
127 			++this.column;
128 			++this.stringPos;
129 		} else if(this.input[this.stringPos] == ',') {
130 			this.cur = Token(TokenType.comma, this.line, this.column);
131 			++this.column;
132 			++this.stringPos;
133 		} else if(this.input[this.stringPos] == '=') {
134 			this.cur = Token(TokenType.equal, this.line, this.column);
135 			++this.column;
136 			++this.stringPos;
137 		} else if(this.input[this.stringPos] == ':') {
138 			this.cur = Token(TokenType.colon, this.line, this.column);
139 			++this.column;
140 			++this.stringPos;
141 		} else {
142 			size_t b = this.stringPos;
143 			size_t e = this.stringPos;
144 			switch(this.input[this.stringPos]) {
145 				case 'm':
146 					++this.stringPos;
147 					++this.column;
148 					++e;
149 					if(this.testCharAndInc('u', e)) {
150 						if(this.testCharAndInc('t', e)) {
151 							if(this.testCharAndInc('a', e)) {
152 								if(this.testCharAndInc('t', e)) {
153 									if(this.testCharAndInc('i', e)) {
154 										if(this.testCharAndInc('o', e)) {
155 											if(this.testCharAndInc('n', e)) {
156 												if(this.isTokenStop()) {
157 													this.cur =
158 														Token(TokenType.mutation,
159 																this.line,
160 																this.column);
161 													return;
162 												}
163 											}
164 										}
165 									}
166 								}
167 							}
168 						}
169 					}
170 					goto default;
171 				case 's':
172 					++this.stringPos;
173 					++this.column;
174 					++e;
175 					if(this.isNotQueryParser() && this.testCharAndInc('u', e)) {
176 						if(this.testCharAndInc('b', e)) {
177 							if(this.testCharAndInc('s', e)) {
178 								if(this.testCharAndInc('c', e)) {
179 									if(this.testCharAndInc('r', e)) {
180 										if(this.testCharAndInc('i', e)) {
181 											if(this.testCharAndInc('p', e)) {
182 												if(this.testCharAndInc('t', e)) {
183 													if(this.testCharAndInc('i', e)) {
184 														if(this.testCharAndInc('o', e)) {
185 															if(this.testCharAndInc('n', e)) {
186 																if(this.isTokenStop()) {
187 																	this.cur =
188 																		Token(TokenType.subscription,
189 																				this.line,
190 																				this.column);
191 																	return;
192 																}
193 															}
194 														}
195 													}
196 												}
197 											}
198 										}
199 									}
200 								}
201 							}
202 						}
203 					} else if(this.isNotQueryParser()
204 								&& this.testCharAndInc('c', e))
205 					{
206 						if(this.testCharAndInc('a', e)) {
207 							if(this.testCharAndInc('l', e)) {
208 								if(this.testCharAndInc('a', e)) {
209 									if(this.testCharAndInc('r', e)) {
210 										if(this.isTokenStop()) {
211 											this.cur = Token(TokenType.scalar, this.line, this.column);
212 											return;
213 										}
214 									}
215 								}
216 							}
217 						} else if(this.isNotQueryParser()
218 									&& this.testCharAndInc('h', e))
219 						{
220 							if(this.testCharAndInc('e', e)) {
221 								if(this.testCharAndInc('m', e)) {
222 									if(this.testCharAndInc('a', e)) {
223 										if(this.isTokenStop()) {
224 											this.cur = Token(TokenType.schema, this.line, this.column);
225 											return;
226 										}
227 									}
228 								}
229 							}
230 						}
231 					}
232 					goto default;
233 				case 'o':
234 					++this.stringPos;
235 					++this.column;
236 					++e;
237 					if(this.testCharAndInc('n', e)) {
238 						if(this.isTokenStop()) {
239 							this.cur = Token(TokenType.on_, this.line,
240 									this.column);
241 							return;
242 						}
243 					}
244 					goto default;
245 				case 'd':
246 					++this.stringPos;
247 					++this.column;
248 					++e;
249 					if(this.testCharAndInc('i', e)) {
250 						if(this.testCharAndInc('r', e)) {
251 							if(this.testCharAndInc('e', e)) {
252 								if(this.testCharAndInc('t', e)) {
253 									if(this.testCharAndInc('i', e)) {
254 										if(this.testCharAndInc('v', e)) {
255 											if(this.testCharAndInc('e', e)) {
256 												if(this.isTokenStop()) {
257 													this.cur = Token(TokenType.directive,
258 															this.line, this.column);
259 													return;
260 												}
261 											}
262 										}
263 									}
264 								}
265 							}
266 						}
267 					}
268 					goto default;
269 				case 'e':
270 					++this.stringPos;
271 					++this.column;
272 					++e;
273 					if(this.testCharAndInc('n', e)) {
274 						if(this.testCharAndInc('u', e)) {
275 							if(this.testCharAndInc('m', e)) {
276 								if(this.isTokenStop()) {
277 									this.cur = Token(TokenType.enum_,
278 											this.line, this.column);
279 									return;
280 								}
281 							}
282 						}
283 					} else if(this.testCharAndInc('x', e)) {
284 						if(this.testCharAndInc('t', e)) {
285 							if(this.testCharAndInc('e', e)) {
286 								if(this.testCharAndInc('n', e)) {
287 									if(this.testCharAndInc('d', e)) {
288 										if(this.isTokenStop()) {
289 											this.cur = Token(TokenType.extend,
290 													this.line, this.column);
291 											return;
292 										}
293 									}
294 								}
295 							}
296 						}
297 					}
298 					goto default;
299 				case 'i':
300 					++this.stringPos;
301 					++this.column;
302 					++e;
303 					if(this.testCharAndInc('n', e)) {
304 						if(this.isNotQueryParser()
305 								&& this.testCharAndInc('p', e)
306 							)
307 						{
308 							if(this.testCharAndInc('u', e)) {
309 								if(this.testCharAndInc('t', e)) {
310 									if(this.isTokenStop()) {
311 										this.cur = Token(TokenType.input,
312 												this.line, this.column);
313 										return;
314 									}
315 								}
316 							}
317 						} else if(this.testCharAndInc('t', e)) {
318 							if(this.testCharAndInc('e', e)) {
319 								if(this.testCharAndInc('r', e)) {
320 									if(this.testCharAndInc('f', e)) {
321 										if(this.testCharAndInc('a', e)) {
322 											if(this.testCharAndInc('c', e)) {
323 												if(this.testCharAndInc('e', e)) {
324 													if(this.isTokenStop()) {
325 														this.cur = Token(TokenType.interface_,
326 																this.line, this.column);
327 														return;
328 													}
329 												}
330 											}
331 										}
332 									}
333 								}
334 							}
335 						}
336 					} else if(this.testCharAndInc('m', e)) {
337 						if(this.testCharAndInc('p', e)) {
338 							if(this.testCharAndInc('l', e)) {
339 								if(this.testCharAndInc('e', e)) {
340 									if(this.testCharAndInc('m', e)) {
341 										if(this.testCharAndInc('e', e)) {
342 											if(this.testCharAndInc('n', e)) {
343 												if(this.testCharAndInc('t', e)) {
344 													if(this.testCharAndInc('s', e)) {
345 														if(this.isTokenStop()) {
346 															this.cur = Token(TokenType.implements,
347 																	this.line, this.column);
348 															return;
349 														}
350 													}
351 												}
352 											}
353 										}
354 									}
355 								}
356 							}
357 						}
358 					}
359 
360 					goto default;
361 				case 'f':
362 					++this.stringPos;
363 					++this.column;
364 					++e;
365 					if(this.testCharAndInc('a', e)) {
366 						if(this.testCharAndInc('l', e)) {
367 							if(this.testCharAndInc('s', e)) {
368 								if(this.testCharAndInc('e', e)) {
369 									if(this.isTokenStop()) {
370 										this.cur = Token(TokenType.false_,
371 												this.line, this.column);
372 										return;
373 									}
374 								}
375 							}
376 						}
377 					} else if(this.testCharAndInc('r', e)) {
378 						if(this.testCharAndInc('a', e)) {
379 							if(this.testCharAndInc('g', e)) {
380 								if(this.testCharAndInc('m', e)) {
381 									if(this.testCharAndInc('e', e)) {
382 										if(this.testCharAndInc('n', e)) {
383 											if(this.testCharAndInc('t', e)) {
384 												if(this.isTokenStop()) {
385 													this.cur =
386 														Token(TokenType.fragment,
387 																this.line,
388 																this.column);
389 													return;
390 												}
391 											}
392 										}
393 									}
394 								}
395 							}
396 						}
397 					}
398 					goto default;
399 				/*case '@':
400 					++this.stringPos;
401 					++this.column;
402 					++e;
403 					if(isTokenStop()) {
404 						this.cur = Token(TokenType.at, this.line, this.column);
405 						return;
406 					}
407 					goto default;*/
408 				case 'q':
409 					++this.stringPos;
410 					++this.column;
411 					++e;
412 					if(this.testCharAndInc('u', e)) {
413 						if(this.testCharAndInc('e', e)) {
414 							if(this.testCharAndInc('r', e)) {
415 								if(this.testCharAndInc('y', e)) {
416 									if(this.isTokenStop()) {
417 										this.cur = Token(TokenType.query,
418 												this.line, this.column);
419 										return;
420 									}
421 								}
422 							}
423 						}
424 					}
425 					goto default;
426 				case 't':
427 					++this.stringPos;
428 					++this.column;
429 					++e;
430 					if(this.testCharAndInc('r', e)) {
431 						if(this.testCharAndInc('u', e)) {
432 							if(this.testCharAndInc('e', e)) {
433 								if(this.isTokenStop()) {
434 									this.cur = Token(TokenType.true_,
435 											this.line, this.column);
436 									return;
437 								}
438 							}
439 						}
440 					} else if(this.isNotQueryParser() &&
441 							this.testCharAndInc('y', e)) {
442 						if(this.testCharAndInc('p', e)) {
443 							if(this.testCharAndInc('e', e)) {
444 								if(this.isTokenStop()) {
445 									this.cur = Token(TokenType.type,
446 											this.line, this.column);
447 									return;
448 								}
449 							}
450 						}
451 					}
452 					goto default;
453 				case 'n':
454 					++this.stringPos;
455 					++this.column;
456 					++e;
457 					if(this.testCharAndInc('u', e)) {
458 						if(this.testCharAndInc('l', e)) {
459 							if(this.testCharAndInc('l', e)) {
460 								if(this.isTokenStop()) {
461 									this.cur = Token(TokenType.null_,
462 											this.line, this.column);
463 									return;
464 								}
465 							}
466 						}
467 					}
468 					goto default;
469 				case 'u':
470 					++this.stringPos;
471 					++this.column;
472 					++e;
473 					if(this.testCharAndInc('n', e)) {
474 						if(this.testCharAndInc('i', e)) {
475 							if(this.testCharAndInc('o', e)) {
476 								if(this.testCharAndInc('n', e)) {
477 									if(this.isTokenStop()) {
478 										this.cur = Token(TokenType.union_,
479 												this.line, this.column);
480 										return;
481 									}
482 								}
483 							}
484 						}
485 					}
486 					goto default;
487 				case '.':
488 					++this.stringPos;
489 					++this.column;
490 					++e;
491 					if(this.testCharAndInc('.', e)) {
492 						if(this.testCharAndInc('.', e)) {
493 							//if(this.stringPos < this.input.length
494 							//	&& isAlphaNum(this.input[this.stringPos]))
495 							if(this.isTokenStop()
496 									|| (this.stringPos < this.input.length
497 										&& isAlphaNum(this.input[this.stringPos])
498 										)
499 								)
500 							{
501 								this.cur = Token(TokenType.dots, this.line,
502 										this.column);
503 								return;
504 							}
505 						}
506 					}
507 					throw new Exception(format(
508 							"failed to parse \"...\" at line %s column %s",
509 							this.line, this.column
510 						));
511 				case '-':
512 					++this.stringPos;
513 					++this.column;
514 					++e;
515 					goto case '0';
516 				case '+':
517 					++this.stringPos;
518 					++this.column;
519 					++e;
520 					goto case '0';
521 				case '0': .. case '9':
522 					do {
523 						++this.stringPos;
524 						++this.column;
525 						++e;
526 					} while(this.stringPos < this.input.length
527 							&& this.input[this.stringPos] >= '0'
528 							&& this.input[this.stringPos] <= '9');
529 
530 					if(this.stringPos >= this.input.length
531 							|| this.input[this.stringPos] != '.')
532 					{
533 						this.cur = Token(TokenType.intValue, this.input[b ..
534 								e], this.line, this.column);
535 						return;
536 					} else if(this.stringPos < this.input.length
537 							&& this.input[this.stringPos] == '.')
538 					{
539 						do {
540 							++this.stringPos;
541 							++this.column;
542 							++e;
543 						} while(this.stringPos < this.input.length
544 								&& this.input[this.stringPos] >= '0'
545 								&& this.input[this.stringPos] <= '9');
546 
547 						this.cur = Token(TokenType.floatValue, this.input[b ..
548 								e], this.line, this.column);
549 						return;
550 					}
551 					goto default;
552 				case '"':
553 					++this.stringPos;
554 					++this.column;
555 					++e;
556 					while(this.stringPos < this.input.length
557 							&& (this.input[this.stringPos] != '"'
558 								|| (this.input[this.stringPos] == '"'
559 									&& this.input[this.stringPos - 1U] == '\\')
560 						 		)
561 						)
562 					{
563 						++this.stringPos;
564 						++this.column;
565 						++e;
566 					}
567 					++this.stringPos;
568 					++this.column;
569 					this.cur = Token(TokenType.stringValue, this.input[b + 1
570 							.. e], this.line, this.column);
571 					break;
572 				default:
573 					while(!this.isTokenStop()) {
574 						//writefln("455 '%s'", this.input[this.stringPos]);
575 						++this.stringPos;
576 						++this.column;
577 						++e;
578 					}
579 					//writefln("%s %s %s '%s'", b, e, this.stringPos, this.input[b .. e]);
580 					//do {
581 					//	writefln("'%s'", this.input[this.stringPos]);
582 					//	++this.stringPos;
583 					//	++this.column;
584 					//	++e;
585 					//} while(!this.isTokenStop());
586 					//writefln("%s %s", TokenType.name, this.input[b .. e]);
587 					this.cur = Token(TokenType.name, this.input[b .. e],
588 							this.line, this.column
589 						);
590 					break;
591 			}
592 		}
593 	}
594 
595 	bool testCharAndInc(const(char) c, ref size_t e) @safe {
596 		if(this.stringPos < this.input.length
597 				&& this.input[this.stringPos] == c)
598 		{
599 			++this.column;
600 			++this.stringPos;
601 			++e;
602 			return true;
603 		} else {
604 			return false;
605 		}
606 	}
607 
608 	@property bool empty() const @safe {
609 		return this.stringPos >= this.input.length
610 			&& this.cur.type == TokenType.undefined;
611 	}
612 
613 	Token front() @property @safe {
614 		return this.cur;
615 	}
616 
617 	@property Token front() const @safe @nogc pure {
618 		return this.cur;
619 	}
620 
621 	void popFront() @safe {
622 		this.buildToken();
623 	}
624 
625 	string getRestOfInput() const @safe {
626 		return this.input[this.stringPos .. $];
627 	}
628 }
629 
630 unittest {
631 	string f = "f ";
632 	auto l = Lexer(f);
633 	assert(!l.empty);
634 	assert(l.front.type == TokenType.name);
635 	assert(l.front.value == "f", format("'%s'", l.front.value));
636 }
637 
638 unittest {
639 	string f = "... ";
640 
641 	auto l = Lexer(f);
642 	assert(!l.empty);
643 	assert(l.front.type == TokenType.dots);
644 	l.popFront();
645 	assert(l.empty);
646 }
647 
648 unittest {
649 	string f = "name! ";
650 	auto l = Lexer(f);
651 	assert(!l.empty);
652 	assert(l.front.type == TokenType.name);
653 	assert(l.front.value == "name", format("'%s'", l.front.value));
654 	l.popFront();
655 	assert(!l.empty);
656 	assert(l.front.type == TokenType.exclamation);
657 	l.popFront();
658 	assert(l.empty);
659 }
660 
661 unittest {
662 	string f = "fragment";
663 	auto l = Lexer(f);
664 	assert(!l.empty);
665 	assert(l.front.type == TokenType.fragment);
666 }
667 
668 unittest {
669 	string f = `
670 		mutation {
671 		  likeStory(storyID: 12345) {
672 		    story {
673 		      likeCount
674 		    }
675 		  }
676 		}`;
677 	auto l = Lexer(f);
678 	assert(!l.empty);
679 	assert(l.front.type == TokenType.mutation);
680 	l.popFront();
681 	assert(!l.empty);
682 	assert(l.front.type == TokenType.lcurly);
683 	l.popFront();
684 	assert(!l.empty);
685 	assert(l.front.type == TokenType.name);
686 	l.popFront();
687 	assert(!l.empty);
688 	assert(l.front.type == TokenType.lparen);
689 	l.popFront();
690 	assert(!l.empty);
691 	assert(l.front.type == TokenType.name);
692 	l.popFront();
693 	assert(!l.empty);
694 	assert(l.front.type == TokenType.colon, format("%s", l.front.type));
695 	l.popFront();
696 	assert(!l.empty);
697 	assert(l.front.type == TokenType.intValue);
698 	l.popFront();
699 	assert(!l.empty);
700 	assert(l.front.type == TokenType.rparen);
701 	l.popFront();
702 	assert(!l.empty);
703 	assert(l.front.type == TokenType.lcurly);
704 	l.popFront();
705 	assert(!l.empty);
706 	assert(l.front.type == TokenType.name);
707 	l.popFront();
708 	assert(!l.empty);
709 	assert(l.front.type == TokenType.lcurly);
710 	l.popFront();
711 	assert(!l.empty);
712 	assert(l.front.type == TokenType.name);
713 	l.popFront();
714 	assert(!l.empty);
715 	assert(l.front.type == TokenType.rcurly);
716 	l.popFront();
717 	assert(!l.empty);
718 	assert(l.front.type == TokenType.rcurly);
719 	l.popFront();
720 	assert(!l.empty);
721 	assert(l.front.type == TokenType.rcurly);
722 	l.popFront();
723 	assert(l.empty);
724 }
725 
726 unittest {
727 	string f = `
728 		query withFragments {
729 		  user(id: +4) {
730 			# super cool comment
731 friends(first: -10.3) {
732 		      ...friendFields
733 			  null false true
734 		    }
735 		    mutualFriends(first: 10) {
736 		      ...friendFields
737 		    }
738 		  }
739 		}
740 
741 		fragment friendFields on User {
742 		  id
743 		  name
744 		  profilePic(size: 50)
745 		}`;
746 	auto l = Lexer(f);
747 	assert(!l.empty);
748 	assert(l.front.type == TokenType.query);
749 	l.popFront();
750 	assert(!l.empty);
751 	assert(l.front.type == TokenType.name);
752 	assert(l.front.value == "withFragments");
753 	l.popFront();
754 	l.popFront();
755 	assert(!l.empty);
756 	assert(l.front.type == TokenType.name);
757 	assert(l.front.value == "user");
758 	l.popFront();
759 	l.popFront();
760 	assert(!l.empty);
761 	assert(l.front.type == TokenType.name);
762 	assert(l.front.value == "id", l.front.value);
763 	l.popFront();
764 	assert(!l.empty);
765 	assert(l.front.type == TokenType.colon);
766 	l.popFront();
767 	assert(!l.empty);
768 	assert(l.front.type == TokenType.intValue);
769 	assert(l.front.value == "+4");
770 	l.popFront();
771 	assert(!l.empty);
772 	assert(l.front.type == TokenType.rparen);
773 	l.popFront();
774 	l.popFront();
775 	assert(!l.empty);
776 	assert(l.front.type == TokenType.name);
777 	assert(l.front.value == "friends");
778 	l.popFront();
779 	assert(!l.empty);
780 	assert(l.front.type == TokenType.lparen);
781 	l.popFront();
782 	assert(!l.empty);
783 	assert(l.front.type == TokenType.name);
784 	assert(l.front.value == "first");
785 	l.popFront();
786 	l.popFront();
787 	assert(!l.empty);
788 	assert(l.front.type == TokenType.floatValue, format("%s", l.front.type));
789 	assert(l.front.value == "-10.3", l.front.value);
790 	l.popFront();
791 	l.popFront();
792 	l.popFront();
793 	assert(!l.empty);
794 	assert(l.front.type == TokenType.dots, format("%s", l.front.type));
795 	l.popFront();
796 	assert(!l.empty);
797 	assert(l.front.type == TokenType.name, format("%s", l.front.type));
798 	assert(l.front.value == "friendFields");
799 	l.popFront();
800 	assert(!l.empty);
801 	assert(l.front.type == TokenType.null_, format("%s", l.front.type));
802 	l.popFront();
803 	assert(!l.empty);
804 	assert(l.front.type == TokenType.false_, format("%s", l.front.type));
805 	l.popFront();
806 	assert(!l.empty);
807 	assert(l.front.type == TokenType.true_, format("%s", l.front.type));
808 	while(!l.empty) {
809 		l.popFront();
810 	}
811 }
812 
813 unittest {
814 	string f = `
815 		query withFragments {
816 		  user(id: "hello") {
817 		  }
818 		}`;
819 
820 	auto l = Lexer(f);
821 	assert(!l.empty);
822 	assert(l.front.type == TokenType.query);
823 	l.popFront();
824 	assert(!l.empty);
825 	assert(l.front.type == TokenType.name);
826 	assert(l.front.value == "withFragments");
827 	l.popFront();
828 	l.popFront();
829 	assert(!l.empty);
830 	assert(l.front.type == TokenType.name);
831 	assert(l.front.value == "user");
832 	l.popFront();
833 	l.popFront();
834 	assert(!l.empty);
835 	assert(l.front.type == TokenType.name);
836 	assert(l.front.value == "id", l.front.value);
837 	l.popFront();
838 	assert(!l.empty);
839 	assert(l.front.type == TokenType.colon);
840 	l.popFront();
841 	assert(!l.empty);
842 	assert(l.front.type == TokenType.stringValue);
843 	assert(l.front.value == "hello", format("'%s' '%s'", l.front.value, "hello"));
844 	l.popFront();
845 	assert(!l.empty);
846 	assert(l.front.type == TokenType.rparen);
847 }