odin-html

HTML Parsing library in Odin.
Log | Files | Refs | README | LICENSE

lex.odin (16447B)


      1 package html
      2 
      3 import "core:log"
      4 import "core:math/rand"
      5 import "core:os"
      6 import "core:path/filepath"
      7 import "core:strconv"
      8 import "core:strings"
      9 import "core:testing"
     10 import "core:time"
     11 import "core:unicode"
     12 
     13 DOCTYPE_LITERAL :: "<!DOCTYPE"
     14 
     15 // Token is a flat descriptor of a lexical token.
     16 //
     17 // `start` and `end` are inclusive byte offsets into the source string.
     18 // For atoms (single-character tokens) `end == start`.
     19 Token :: struct {
     20 	kind:  Token_Kind,
     21 	start: u32,
     22 	end:   u32,
     23 }
     24 
     25 Token_Kind :: enum u8 {
     26 	Open_Tag_Start,
     27 	Tag_End,
     28 	Close_Tag_Start,
     29 	Self_Closing_Tag_End,
     30 	Assign,
     31 	Comment_Start,
     32 	Comment_End,
     33 	Double_Quote_String_Literal,
     34 	Single_Quote_String_Literal,
     35 	Raw_Text,
     36 	Doctype,
     37 }
     38 
     39 Lexer :: struct {
     40 	cursor: u32,
     41 	source: string,
     42 }
     43 
     44 lexer_next :: proc(l: ^Lexer) -> (t: Token, ok: bool) {
     45 	if peek := lexer_peek(l, len(DOCTYPE_LITERAL)); peek == DOCTYPE_LITERAL {
     46 		start := l.cursor
     47 		if !lexer_advance_until(l, `>`) do return t, false
     48 		end := l.cursor
     49 		defer lexer_advance(l)
     50 		return Token{kind = .Doctype, start = start, end = end}, true
     51 	}
     52 
     53 	peek := lexer_peek(l)
     54 	if peek == "" {
     55 		return t, false
     56 	}
     57 
     58 	if t, ok := lex_token(l); ok {
     59 		return t, true
     60 	}
     61 
     62 	// Advance until we find an interesting token.
     63 	// The cursor is reset to undo the advancing that occurs within lex_token.
     64 	start := l.cursor
     65 	for {
     66 		if !lexer_advance(l) {
     67 			break
     68 		}
     69 
     70 		start := l.cursor
     71 		_, ok := lex_token(l)
     72 		l.cursor = start
     73 
     74 		if ok {
     75 			break
     76 		}
     77 	}
     78 	end := l.cursor - 1
     79 
     80 	return Token{kind = .Raw_Text, start = start, end = end}, true
     81 }
     82 
     83 @(private = "file")
     84 lex_token :: proc(l: ^Lexer) -> (t: Token, ok: bool) {
     85 	if peek := lexer_peek(l, 4); peek == "<!--" {
     86 		defer lexer_advance(l, 4)
     87 		return Token{kind = .Comment_Start, start = l.cursor, end = l.cursor + 3}, true
     88 	}
     89 
     90 	if peek := lexer_peek(l, 3); peek == "-->" {
     91 		defer lexer_advance(l, 3)
     92 		return Token{kind = .Comment_End, start = l.cursor, end = l.cursor + 2}, true
     93 	}
     94 
     95 	if peek := lexer_peek(l, 2); peek != "" && peek[0] == '<' && unicode.is_letter(rune(peek[1])) {
     96 		lexer_advance(l)
     97 		start := l.cursor
     98 		if !lexer_advance_while_letters(l) do return
     99 		end := l.cursor
    100 		defer lexer_advance(l)
    101 		return Token{kind = .Open_Tag_Start, start = start, end = end}, true
    102 	}
    103 
    104 	if peek := lexer_peek(l, 2); peek == "</" {
    105 		lexer_advance(l, 2)
    106 		start := l.cursor
    107 		if !lexer_advance_while_letters(l) do return
    108 		end := l.cursor
    109 		defer lexer_advance(l)
    110 		return Token{kind = .Close_Tag_Start, start = start, end = end}, true
    111 	}
    112 
    113 	if peek := lexer_peek(l, 2); peek == "/>" {
    114 		defer lexer_advance(l, 2)
    115 		return Token{kind = .Self_Closing_Tag_End, start = l.cursor, end = l.cursor + 1}, true
    116 	}
    117 
    118 	if peek := lexer_peek(l, 1); peek == ">" {
    119 		defer lexer_advance(l, 1)
    120 		return Token{kind = .Tag_End, start = l.cursor, end = l.cursor}, true
    121 	}
    122 
    123 	if peek := lexer_peek(l, 1); peek == "=" {
    124 		defer lexer_advance(l, 1)
    125 		return Token{kind = .Assign, start = l.cursor, end = l.cursor}, true
    126 	}
    127 
    128 	if peek := lexer_peek(l, 1); peek == `"` {
    129 		start := l.cursor
    130 		lexer_advance(l, 1)
    131 		if !lexer_advance_until(l, `"`) do return t, false
    132 		end := l.cursor
    133 		defer lexer_advance(l, 1)
    134 		return Token{kind = .Double_Quote_String_Literal, start = start, end = end}, true
    135 	}
    136 
    137 	if peek := lexer_peek(l, 1); peek == `'` {
    138 		start := l.cursor
    139 		lexer_advance(l, 1)
    140 		if !lexer_advance_until(l, `'`) do return t, false
    141 		end := l.cursor
    142 		defer lexer_advance(l, 1)
    143 		return Token{kind = .Single_Quote_String_Literal, start = start, end = end}, true
    144 	}
    145 
    146 	return
    147 
    148 }
    149 
    150 lexer_expect :: proc(l: ^Lexer, kind: Token_Kind) -> (t: Token, ok: bool) {
    151 	t = lexer_next(l) or_return
    152 	return t, token_kind(t) == kind
    153 }
    154 
    155 lexer_advance :: proc(l: ^Lexer, step: u32 = 1) -> (ok: bool) {
    156 	l.cursor += step
    157 	return l.cursor < u32(len(l.source))
    158 }
    159 
    160 // lexer_advance_until advances the cursor until the source at the cursor
    161 // matches `target`. Returns true if a match was found, false on EOF.
    162 lexer_advance_until :: proc(l: ^Lexer, target: string) -> bool {
    163 	for {
    164 		if lexer_peek(l, u32(len(target))) == target {
    165 			return true
    166 		}
    167 		if !lexer_advance(l) {
    168 			return false
    169 		}
    170 	}
    171 }
    172 
    173 // lexer_advance_while_letters advances the cursor while the byte at cursor+1
    174 // is a letter, hyphen, or underscore. Returns true if stopped at a non-identifier
    175 // byte, false on EOF.
    176 lexer_advance_while_letters :: proc(l: ^Lexer) -> bool {
    177 	for {
    178 		b, ok := lexer_peek_byte(l, 1)
    179 		if !ok {
    180 			return false
    181 		}
    182 		if !(unicode.is_letter(rune(b)) || b == '-' || b == '_') {
    183 			return true
    184 		}
    185 		if !lexer_advance(l) {
    186 			return false
    187 		}
    188 	}
    189 }
    190 
    191 // lexer_advance_until_space advances the cursor until the byte at cursor+1
    192 // is whitespace. Returns true if stopped, false on EOF.
    193 lexer_advance_until_space :: proc(l: ^Lexer) -> bool {
    194 	for {
    195 		b, ok := lexer_peek_byte(l, 1)
    196 		if !ok {
    197 			return false
    198 		}
    199 		if unicode.is_space(rune(b)) {
    200 			return true
    201 		}
    202 		if !lexer_advance(l) {
    203 			return false
    204 		}
    205 	}
    206 }
    207 
    208 // lexer_advance_until_space_or advances the cursor until the byte at cursor+1
    209 // is whitespace or equals `or`. Returns true if stopped, false on EOF.
    210 lexer_advance_until_space_or :: proc(l: ^Lexer, or: byte) -> bool {
    211 	for {
    212 		b, ok := lexer_peek_byte(l, 1)
    213 		if !ok {
    214 			return false
    215 		}
    216 		if unicode.is_space(rune(b)) || b == or {
    217 			return true
    218 		}
    219 		if !lexer_advance(l) {
    220 			return false
    221 		}
    222 	}
    223 }
    224 
    225 lexer_peek :: proc(l: ^Lexer, step: u32 = 1) -> (p: string) {
    226 	start := l.cursor
    227 	end := l.cursor + step
    228 
    229 	if end > u32(len(l.source)) {
    230 		return ""
    231 	}
    232 
    233 	return l.source[start:end]
    234 }
    235 
    236 // lexer_peek_byte returns the byte at cursor+offset without advancing.
    237 lexer_peek_byte :: proc(l: ^Lexer, offset: u32 = 0) -> (b: byte, ok: bool) {
    238 	pos := l.cursor + offset
    239 	if pos >= u32(len(l.source)) {
    240 		return 0, false
    241 	}
    242 	return l.source[pos], true
    243 }
    244 
    245 // lexer_peek_token returns the next token without advancing the lexer.
    246 lexer_peek_token :: proc(l: ^Lexer) -> (t: Token, ok: bool) {
    247 	start := l.cursor
    248 	defer l.cursor = start
    249 	return lexer_next(l)
    250 }
    251 
    252 lexer_skip_space :: proc(l: ^Lexer) {
    253 	for {
    254 		b, ok := lexer_peek_byte(l)
    255 		if !ok {
    256 			return
    257 		}
    258 		if !unicode.is_space(rune(b)) {
    259 			return
    260 		}
    261 		if !lexer_advance(l) {
    262 			return
    263 		}
    264 	}
    265 }
    266 
    267 // token_kind returns the kind for the token.
    268 token_kind :: proc(t: Token) -> Token_Kind {
    269 	return t.kind
    270 }
    271 
    272 // token_lookup returns the substring corresponding to the token.
    273 // For atoms (end == start) this is the single byte at `start`;
    274 // for spans it is the inclusive range [start, end].
    275 token_lookup :: proc(t: Token, text: string) -> string {
    276 	return text[t.start:t.end + 1]
    277 }
    278 
    279 @(private = "file")
    280 check_tokens :: proc(t: ^testing.T, source: string, want: []Token, loc := #caller_location) {
    281 	defer free_all()
    282 
    283 	lexer: Lexer
    284 	lexer.source = source
    285 
    286 	got: [dynamic]Token
    287 
    288 	defer if testing.failed(t) {
    289 		log.debugf("got: %v", got)
    290 	}
    291 
    292 	for ii := 0; true; ii += 1 {
    293 		token, ok := lexer_next(&lexer)
    294 		append(&got, token)
    295 
    296 		log.debugf("%d/%d: token %v %v", ii + 1, len(want), ok, token, location = loc)
    297 		log.debugf("lexer: %v", lexer, location = loc)
    298 
    299 		if !ok {
    300 			if ii <= len(want) - 1 {
    301 				log.errorf(
    302 					"short output: got %d tokens; (%v), want %d %v",
    303 					ii,
    304 					token,
    305 					len(want),
    306 					want,
    307 					location = loc,
    308 				)
    309 			}
    310 			break
    311 		}
    312 
    313 		if ii >= len(want) {
    314 			log.errorf("got more tokens than expected: %v", token, location = loc)
    315 		} else {
    316 			testing.expect_value(t, token, want[ii], loc = loc)
    317 		}
    318 	}
    319 }
    320 
    321 @(test)
    322 test_lex_comment :: proc(t: ^testing.T) {
    323 	source := "<!-- comment -->"
    324 	want := []Token {
    325 		Token{kind = .Comment_Start, start = 0, end = 3},
    326 		Token{kind = .Raw_Text, start = 4, end = 12},
    327 		Token{kind = .Comment_End, start = 13, end = 15},
    328 	}
    329 	check_tokens(t, source, want)
    330 }
    331 
    332 @(test)
    333 test_lex_comment_2 :: proc(t: ^testing.T) {
    334 	source := "<!--comment-->"
    335 	want := []Token {
    336 		Token{kind = .Comment_Start, start = 0, end = 3},
    337 		Token{kind = .Raw_Text, start = 4, end = 10},
    338 		Token{kind = .Comment_End, start = 11, end = 13},
    339 	}
    340 	check_tokens(t, source, want)
    341 }
    342 
    343 @(test)
    344 test_lex_comment_3 :: proc(t: ^testing.T) {
    345 	source := "<!-- \ncomment\n -->"
    346 	want := []Token {
    347 		Token{kind = .Comment_Start, start = 0, end = 3},
    348 		Token{kind = .Raw_Text, start = 4, end = 14},
    349 		Token{kind = .Comment_End, start = 15, end = 17},
    350 	}
    351 	check_tokens(t, source, want)
    352 }
    353 
    354 @(test)
    355 test_lex_doctype :: proc(t: ^testing.T) {
    356 	source := "<!DOCTYPE html>"
    357 	want := []Token{Token{kind = .Doctype, start = 0, end = 14}}
    358 	check_tokens(t, source, want)
    359 }
    360 
    361 @(test)
    362 test_lex_exotic_doctype :: proc(t: ^testing.T) {
    363 	source := "<!DOCTYPE foobar>"
    364 	want := []Token{Token{kind = .Doctype, start = 0, end = 16}}
    365 	check_tokens(t, source, want)
    366 }
    367 
    368 @(test)
    369 test_lex_self_closing_tag :: proc(t: ^testing.T) {
    370 	source := `<tag />`
    371 	want := []Token {
    372 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    373 		Token{kind = .Raw_Text, start = 4, end = 4},
    374 		Token{kind = .Self_Closing_Tag_End, start = 5, end = 6},
    375 	}
    376 	check_tokens(t, source, want)
    377 }
    378 
    379 @(test)
    380 test_lex_self_closing_tag_2 :: proc(t: ^testing.T) {
    381 	source := `<tag/>`
    382 	want := []Token {
    383 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    384 		Token{kind = .Self_Closing_Tag_End, start = 4, end = 5},
    385 	}
    386 	check_tokens(t, source, want)
    387 }
    388 
    389 @(test)
    390 test_lex_open_tag :: proc(t: ^testing.T) {
    391 	source := "<tag>"
    392 	want := []Token {
    393 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    394 		Token{kind = .Tag_End, start = 4, end = 4},
    395 	}
    396 	check_tokens(t, source, want)
    397 }
    398 
    399 @(test)
    400 test_lex_close_tag :: proc(t: ^testing.T) {
    401 	source := "</tag>"
    402 	want := []Token {
    403 		Token{kind = .Close_Tag_Start, start = 2, end = 4},
    404 		Token{kind = .Tag_End, start = 5, end = 5},
    405 	}
    406 	check_tokens(t, source, want)
    407 }
    408 
    409 @(test)
    410 test_lex_tag_pair :: proc(t: ^testing.T) {
    411 	source := "<tag></tag>"
    412 	want := []Token {
    413 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    414 		Token{kind = .Tag_End, start = 4, end = 4},
    415 		Token{kind = .Close_Tag_Start, start = 7, end = 9},
    416 		Token{kind = .Tag_End, start = 10, end = 10},
    417 	}
    418 	check_tokens(t, source, want)
    419 }
    420 
    421 @(test)
    422 test_lex_tag_pair_with_comment :: proc(t: ^testing.T) {
    423 	source := "<tag><!--comment--></tag>"
    424 	want := []Token {
    425 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    426 		Token{kind = .Tag_End, start = 4, end = 4},
    427 		Token{kind = .Comment_Start, start = 5, end = 8},
    428 		Token{kind = .Raw_Text, start = 9, end = 15},
    429 		Token{kind = .Comment_End, start = 16, end = 18},
    430 		Token{kind = .Close_Tag_Start, start = 21, end = 23},
    431 		Token{kind = .Tag_End, start = 24, end = 24},
    432 	}
    433 	check_tokens(t, source, want)
    434 }
    435 
    436 @(test)
    437 test_lex_open_tag_value_attribute :: proc(t: ^testing.T) {
    438 	source := `<tag class=".style">`
    439 	want := []Token {
    440 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    441 		Token{kind = .Raw_Text, start = 4, end = 9},
    442 		Token{kind = .Assign, start = 10, end = 10},
    443 		Token{kind = .Double_Quote_String_Literal, start = 11, end = 18},
    444 		Token{kind = .Tag_End, start = 19, end = 19},
    445 	}
    446 	check_tokens(t, source, want)
    447 }
    448 
    449 @(test)
    450 test_lex_open_tag_naked_attribute :: proc(t: ^testing.T) {
    451 	source := `<tag class=".style" foobar>`
    452 	want := []Token {
    453 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    454 		Token{kind = .Raw_Text, start = 4, end = 9},
    455 		Token{kind = .Assign, start = 10, end = 10},
    456 		Token{kind = .Double_Quote_String_Literal, start = 11, end = 18},
    457 		Token{kind = .Raw_Text, start = 19, end = 25},
    458 		Token{kind = .Tag_End, start = 26, end = 26},
    459 	}
    460 	check_tokens(t, source, want)
    461 }
    462 
    463 @(test)
    464 test_lex_nested_tags :: proc(t: ^testing.T) {
    465 	source := `<tag><ul><li>one</li><li>two</li></ul></tag>`
    466 	want := []Token {
    467 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    468 		Token{kind = .Tag_End, start = 4, end = 4},
    469 		Token{kind = .Open_Tag_Start, start = 6, end = 7},
    470 		Token{kind = .Tag_End, start = 8, end = 8},
    471 		Token{kind = .Open_Tag_Start, start = 10, end = 11},
    472 		Token{kind = .Tag_End, start = 12, end = 12},
    473 		Token{kind = .Raw_Text, start = 13, end = 15},
    474 		Token{kind = .Close_Tag_Start, start = 18, end = 19},
    475 		Token{kind = .Tag_End, start = 20, end = 20},
    476 		Token{kind = .Open_Tag_Start, start = 22, end = 23},
    477 		Token{kind = .Tag_End, start = 24, end = 24},
    478 		Token{kind = .Raw_Text, start = 25, end = 27},
    479 		Token{kind = .Close_Tag_Start, start = 30, end = 31},
    480 		Token{kind = .Tag_End, start = 32, end = 32},
    481 		Token{kind = .Close_Tag_Start, start = 35, end = 36},
    482 		Token{kind = .Tag_End, start = 37, end = 37},
    483 		Token{kind = .Close_Tag_Start, start = 40, end = 42},
    484 		Token{kind = .Tag_End, start = 43, end = 43},
    485 	}
    486 	check_tokens(t, source, want)
    487 }
    488 
    489 @(test)
    490 test_lex_raw_text_less_than :: proc(t: ^testing.T) {
    491 	source := `<tag> 1 < 2 </tag>`
    492 	want := []Token {
    493 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    494 		Token{kind = .Tag_End, start = 4, end = 4},
    495 		Token{kind = .Raw_Text, start = 5, end = 11},
    496 		Token{kind = .Close_Tag_Start, start = 14, end = 16},
    497 		Token{kind = .Tag_End, start = 17, end = 17},
    498 	}
    499 	check_tokens(t, source, want)
    500 }
    501 
    502 @(test)
    503 test_lex_invalid_tag :: proc(t: ^testing.T) {
    504 	source := `<tag> one <two three </tag>`
    505 	want := []Token {
    506 		Token{kind = .Open_Tag_Start, start = 1, end = 3},
    507 		Token{kind = .Tag_End, start = 4, end = 4},
    508 		Token{kind = .Raw_Text, start = 5, end = 9},
    509 		Token{kind = .Open_Tag_Start, start = 11, end = 13},
    510 		Token{kind = .Raw_Text, start = 14, end = 20},
    511 		Token{kind = .Close_Tag_Start, start = 23, end = 25},
    512 		Token{kind = .Tag_End, start = 26, end = 26},
    513 	}
    514 	check_tokens(t, source, want)
    515 }
    516 
    517 FUZZ_ENABLED :: #config(FUZZ, false)
    518 FUZZ_RUN_FAILED :: #config(FUZZ_RUN_FAILED, false)
    519 
    520 @(test)
    521 @(disabled = !FUZZ_ENABLED)
    522 test_lex_fuzz_lexer :: proc(t: ^testing.T) {
    523 	if !FUZZ_ENABLED do return
    524 
    525 	target: string
    526 
    527 	// cleanup saves the failed fuzz target to a file.
    528 	testing.cleanup(t, proc(userdata: rawptr) {
    529 			target: ^string = auto_cast userdata
    530 
    531 			if target == nil || target^ == "" {
    532 				return
    533 			}
    534 
    535 			buf: [64]byte
    536 
    537 			name := strconv.write_int(buf[:], i64(time.time_to_unix(time.now())), 10)
    538 
    539 			if err := os.make_directory("fuzzdata"); err != nil && err != .Exist {
    540 				log.errorf("preparing fuzzdata directory: %w", err)
    541 			}
    542 
    543 			file_path, _ := filepath.join([]string{"fuzzdata", name})
    544 
    545 			if err := os.write_entire_file_from_string(file_path, target^); err != nil {
    546 				log.errorf("writing to fuzz corpus file: %v", err)
    547 			}
    548 
    549 		}, &target)
    550 
    551 	log.info("fuzzing ...")
    552 
    553 	// Run the known failed fuzz corpus.
    554 	if FUZZ_RUN_FAILED {
    555 		dir, err := os.open("fuzzdata")
    556 		if err != nil {
    557 			log.errorf("opening fuzzdata directory: %v", err)
    558 		}
    559 
    560 		defer os.close(dir)
    561 
    562 		entries, entries_err := os.read_directory(dir, -1, context.allocator)
    563 		if entries_err != nil {
    564 			log.errorf("reading fuzzdata entries: %v", err)
    565 		}
    566 
    567 		defer {
    568 			for entry in entries {
    569 				os.file_info_delete(entry, context.allocator)
    570 			}
    571 			delete(entries)
    572 		}
    573 
    574 		for entry in entries {
    575 			input, input_err := os.read_entire_file_from_path(entry.fullpath, context.allocator)
    576 			if input_err != nil {
    577 				log.errorf("reading fuzzdata entry: %v", err)
    578 				continue
    579 			}
    580 
    581 			l: Lexer
    582 			l.source = string(input)
    583 
    584 			log.debugf("%q", l.source)
    585 			lexer_print_tokens(&l)
    586 		}
    587 
    588 		return
    589 	}
    590 
    591 	// Explore new randomized inputs.
    592 
    593 	rand.reset(t.seed)
    594 
    595 	// Define the pool characters.
    596 	// Whitespace and angle brackets are weighted higher.
    597 	character_set :: "abcdefghijklmnopqrstuvwxyz        <<<<<>>>>>/=\"!"
    598 
    599 	for ii := 1; true; ii += 1 {
    600 		source := random_string(character_set, rand.int_max(100))
    601 		defer strings.builder_destroy(&source)
    602 
    603 		src := strings.to_string(source)
    604 		target = src
    605 
    606 		l: Lexer
    607 		l.source = src
    608 
    609 		for {
    610 			_, ok := lexer_next(&l)
    611 			if !ok {
    612 				break
    613 			}
    614 		}
    615 	}
    616 }
    617 
    618 lexer_print_tokens :: proc(l: ^Lexer) {
    619 	for token in lexer_next(l) {
    620 		context.allocator = context.temp_allocator
    621 		defer free_all()
    622 		if token.start == token.end {
    623 			log.debug(token)
    624 			log.debugf("\t: %q \t(%v)", rune(l.source[token.start]), token)
    625 			log.debugf("\t: %v", l.source)
    626 			log.debugf("\t: %v^", strings.repeat(" ", int(token.start)))
    627 		} else {
    628 			log.debugf("\t: %q \t(%v)", l.source[token.start:token.end + 1], token)
    629 			log.debugf("\t: %v", l.source)
    630 			padding := strings.repeat(" ", int(token.start))
    631 			underline := ""
    632 			if token.end - token.start - 1 >= 0 {
    633 				underline = strings.repeat("_", int(token.end - token.start - 1))
    634 			}
    635 			log.debugf("\t: %v^%v^", padding, underline)
    636 		}
    637 	}
    638 }
    639 
    640 random_string :: proc(char_set: string, length: int) -> strings.Builder {
    641 	b: strings.Builder
    642 
    643 	for ii in 0 ..< length {
    644 		strings.write_byte(&b, char_set[rand.int_max(len(char_set))])
    645 	}
    646 
    647 	return b
    648 }
    649