// Copyright (c) 2019 Alexander Medvednikov. All rights reserved. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. module main struct Scanner { mut: file_path string text string pos int line_nr int inside_string bool dollar_start bool // for hacky string interpolation TODO simplify dollar_end bool debug bool line_comment string started bool is_fmt bool // vfmt fields fmt_out StringBuilder fmt_indent int fmt_line_empty bool } const ( SINGLE_QUOTE = `\'` //QUOTE = `"` ) fn new_scanner(file_path string) *Scanner { if !os.file_exists(file_path) { panic('"$file_path" doesn\'t exist') } text := os.read_file(file_path) or { panic('scanner: failed to open "$file_path"') return &Scanner{} } scanner := &Scanner { file_path: file_path text: text fmt_out: new_string_builder(1000) } // println('new scanner "$file_path" txt.len=$scanner.text.len') return scanner } // TODO remove once multiple return values are implemented struct ScanRes { tok Token lit string } fn scan_res(tok Token, lit string) ScanRes { return ScanRes{tok, lit} } fn is_white(c byte) bool { return c.is_white() } fn is_nl(c byte) bool { return c == `\r` || c == `\n` } fn (s mut Scanner) ident_name() string { start := s.pos for { s.pos++ if s.pos >= s.text.len { break } c := s.text[s.pos] if !is_name_char(c) && !c.is_digit() { break } } name := s.text.substr(start, s.pos) s.pos-- return name } fn (s mut Scanner) ident_number() string { start := s.pos is_hex := s.pos + 1 < s.text.len && s.text[s.pos] == `0` && s.text[s.pos + 1] == `x` is_oct := !is_hex && s.text[s.pos] == `0` mut is_float := false for { s.pos++ if s.pos >= s.text.len { break } c := s.text[s.pos] if c == `.` { is_float = true } is_good_hex := is_hex && (c == `x` || (c >= `a` && c <= `f`)) // 1e+3, 1e-3, 1e3 if !is_hex && c == `e` && s.pos + 1 < s.text.len { next := s.text[s.pos + 1] if next == `+` || next == `-` || next.is_digit() { s.pos++ continue } } if !c.is_digit() && c != `.` && !is_good_hex { break } // 1..9 if c == `.` && s.pos + 1 < s.text.len && s.text[s.pos + 1] == `.` { break } if is_oct && c >= `8` && !is_float { s.error('malformed octal constant') } } number := s.text.substr(start, s.pos) s.pos-- return number } fn (s mut Scanner) skip_whitespace() { for s.pos < s.text.len && is_white(s.text[s.pos]) { if is_nl(s.text[s.pos]) { s.line_nr++ if s.is_fmt { return } } s.pos++ } // if s.pos == s.text.len { // return scan_res(EOF, '') // } } fn (s mut Scanner) get_var_name(pos int) string { mut pos_start := pos for ; pos_start >= 0 && s.text[pos_start] != `\n` && s.text[pos_start] != `;`; pos_start-- {} pos_start++ return s.text.substr(pos_start, pos) } // CAO stands for Compound Assignment Operators (e.g '+=' ) fn (s mut Scanner) cao_change(operator string) { s.text = s.text.substr(0, s.pos - operator.len) + ' = ' + s.get_var_name(s.pos - operator.len) + ' ' + operator + ' ' + s.text.substr(s.pos + 1, s.text.len) } fn (s mut Scanner) scan() ScanRes { // if s.file_path == 'd.v' { // println('\nscan()') // } if s.started { // || (s.pos == 0 && s.text.len > 0 && s.text[s.pos] == `\n`) { s.pos++ } s.started = true if s.pos >= s.text.len { return scan_res(EOF, '') } // skip whitespace if !s.inside_string { s.skip_whitespace() } if s.is_fmt && s.text[s.pos] == `\n` { return scan_res(NL, '') } // End of $var, start next string if !s.is_fmt && s.dollar_end { // fmt.Println("end of $var, get string", s.pos, string(s.text[s.pos])) if s.text[s.pos] == SINGLE_QUOTE { // fmt.Println("ENDDD") s.dollar_end = false return scan_res(STRING, '') } s.dollar_end = false return scan_res(STRING, s.ident_string()) } s.skip_whitespace() // end of file if s.pos >= s.text.len { // println('scan(): returning EOF (pos >= len)') return scan_res(EOF, '') } // handle each char c := s.text[s.pos] mut nextc := `\0` if s.pos + 1 < s.text.len { nextc = s.text[s.pos + 1] } // name or keyword if is_name_char(c) { name := s.ident_name() // tmp hack to detect . in ${} // Check if not EOF to prevent panic next_char := if s.pos + 1 < s.text.len { s.text[s.pos + 1] } else { `\0` } // println('!!! got name=$name next_char=$next_char') if is_key(name) { // println('IS KEY') // tok := (key_to_token(name)) // println(tok.str()) return scan_res(key_to_token(name), '') } // 'asdf $b' => "b" is the last name in the string, dont start parsing string // at the next ', skip it if s.inside_string { // println('is_letter inside string! nextc=${nextc.str()}') if next_char == SINGLE_QUOTE { // println('var is last before QUOTE') s.pos++ s.dollar_start = false s.inside_string = false } } if s.dollar_start && next_char != `.` { // println('INSIDE STRING .dollar var=$name') s.dollar_end = true s.dollar_start = false } if s.pos == 0 && next_char == ` ` { s.pos++ //If a single letter name at the start of the file, increment //Otherwise the scanner would be stuck at s.pos = 0 } return scan_res(NAME, name) } // number, `.123` else if c.is_digit() || c == `.` && nextc.is_digit() { num := s.ident_number() return scan_res(INT, num) } // all other tokens switch c { case `+`: if nextc == `+` { s.pos++ return scan_res(INC, '') } else if nextc == `=` { s.pos++ s.cao_change('+') return scan_res(PLUS_ASSIGN, '') } return scan_res(PLUS, '') case `-`: if nextc == `-` { s.pos++ return scan_res(DEC, '') } else if nextc == `=` { s.pos++ s.cao_change('-') return scan_res(MINUS_ASSIGN, '') } return scan_res(MINUS, '') case `*`: if nextc == `=` { s.pos++ s.cao_change('*') return scan_res(MULT_ASSIGN, '') } return scan_res(MUL, '') case `^`: if nextc == `=` { s.pos++ s.cao_change('^') return scan_res(XOR_ASSIGN, '') } return scan_res(XOR, '') case `%`: if nextc == `=` { s.pos++ s.cao_change('%') return scan_res(MOD_ASSIGN, '') } return scan_res(MOD, '') case `?`: return scan_res(QUESTION, '') case SINGLE_QUOTE: return scan_res(STRING, s.ident_string()) // TODO allow double quotes // case QUOTE: // return scan_res(STRING, s.ident_string()) case `\``: return scan_res(CHAR, s.ident_char()) case `(`: return scan_res(LPAR, '') case `)`: return scan_res(RPAR, '') case `[`: return scan_res(LSBR, '') case `]`: return scan_res(RSBR, '') case `{`: // Skip { in ${ in strings if s.inside_string { return s.scan() } return scan_res(LCBR, '') case `$`: return scan_res(DOLLAR, '') case `}`: // s = `hello $name kek` // s = `hello ${name} kek` if s.inside_string { s.pos++ // TODO UNNEEDED? if s.text[s.pos] == SINGLE_QUOTE { s.inside_string = false return scan_res(STRING, '') } return scan_res(STRING, s.ident_string()) } else { return scan_res(RCBR, '') } case `&`: if nextc == `=` { s.pos++ s.cao_change('&') return scan_res(AND_ASSIGN, '') } if nextc == `&` { s.pos++ return scan_res(AND, '') } return scan_res(AMP, '') case `|`: if nextc == `|` { s.pos++ return scan_res(OR, '') } if nextc == `=` { s.pos++ s.cao_change('|') return scan_res(OR_ASSIGN, '') } return scan_res(PIPE, '') case `,`: return scan_res(COMMA, '') case `\r`: if nextc == `\n` { s.pos++ return scan_res(NL, '') } case `\n`: return scan_res(NL, '') case `.`: if nextc == `.` { s.pos++ return scan_res(DOTDOT, '') } return scan_res(DOT, '') case `#`: start := s.pos + 1 for s.pos < s.text.len && s.text[s.pos] != `\n` { s.pos++ } s.line_nr++ hash := s.text.substr(start, s.pos) if s.is_fmt { // fmt needs NL after # s.pos-- } return scan_res(HASH, hash.trim_space()) case `>`: if nextc == `=` { s.pos++ return scan_res(GE, '') } else if nextc == `>` { if s.pos + 2 < s.text.len && s.text[s.pos + 2] == `=` { s.pos += 2 s.cao_change('>>') return scan_res(RIGHT_SHIFT_ASSIGN, '') } s.pos++ return scan_res(RIGHT_SHIFT, '') } else { return scan_res(GT, '') } case `<`: if nextc == `=` { s.pos++ return scan_res(LE, '') } else if nextc == `<` { if s.pos + 2 < s.text.len && s.text[s.pos + 2] == `=` { s.pos += 2 s.cao_change('<<') return scan_res(LEFT_SHIFT_ASSIGN, '') } s.pos++ return scan_res(LEFT_SHIFT, '') } else { return scan_res(LT, '') } case `=`: if nextc == `=` { s.pos++ return scan_res(EQ, '') } else { return scan_res(ASSIGN, '') } case `:`: if nextc == `=` { s.pos++ return scan_res(DECL_ASSIGN, '') } else { return scan_res(COLON, '') } case `;`: return scan_res(SEMICOLON, '') case `!`: if nextc == `=` { s.pos++ return scan_res(NE, '') } else { return scan_res(NOT, '') } case `~`: return scan_res(BIT_NOT, '') case `/`: if nextc == `=` { s.pos++ s.cao_change('/') return scan_res(DIV_ASSIGN, '') } if nextc == `/` { // debug("!!!!!!GOT LINE COM") start := s.pos + 1 for s.pos < s.text.len && s.text[s.pos] != `\n`{ s.pos++ } s.line_nr++ s.line_comment = s.text.substr(start + 1, s.pos) s.line_comment = s.line_comment.trim_space() s.fgenln('// $s.line_comment') if s.is_fmt { // fmt needs NL after comment s.pos-- } else { // Skip comment return s.scan() } return scan_res(LINE_COM, s.line_comment) } // Multiline comments if nextc == `*` { start := s.pos mut nest_count := 1 // Skip comment for nest_count > 0 { s.pos++ if s.pos >= s.text.len { s.line_nr-- s.error('comment not terminated') } if s.text[s.pos] == `\n` { s.line_nr++ continue } if s.text[s.pos] == `/` && s.text[s.pos + 1] == `*` { nest_count++ continue } if s.text[s.pos] == `*` && s.text[s.pos + 1] == `/` { nest_count-- } } s.pos++ end := s.pos + 1 comm := s.text.substr(start, end) s.fgenln(comm) if s.is_fmt { return scan_res(MLINE_COM, comm) } // Skip if not in fmt mode return s.scan() } return scan_res(DIV, '') } $if windows { if c == `\0` { return scan_res(EOF, '') } } println('(char code=$c) pos=$s.pos len=$s.text.len') s.error('invalid character `${c.str()}`') return scan_res(EOF, '') } fn (s &Scanner) error(msg string) { file := s.file_path.all_after('/') println('panic: $file:${s.line_nr + 1}') println(msg) // os.print_backtrace() // println(file) // println(s.file_path) exit(1) } // println('array out of bounds $idx len=$a.len') // This is really bad. It needs a major clean up fn (s mut Scanner) ident_string() string { // println("\nidentString() at char=", string(s.text[s.pos]), // "chard=", s.text[s.pos], " pos=", s.pos, "txt=", s.text[s.pos:s.pos+7]) debug := s.file_path.contains('test_test') if debug { println('identStr() $s.file_path line=$s.line_nr pos=$s.pos') } mut start := s.pos s.inside_string = false slash := `\\` for { s.pos++ if s.pos >= s.text.len { break } c := s.text[s.pos] if debug { println(c.str()) } prevc := s.text[s.pos - 1] // end of string if c == SINGLE_QUOTE && (prevc != slash || (prevc == slash && s.text[s.pos - 2] == slash)) { // handle '123\\' slash at the end break } if c == `\n` { s.line_nr++ } // Don't allow \0 if c == `0` && s.pos > 2 && s.text[s.pos - 1] == `\\` { s.error('0 character in a string literal') } // Don't allow \x00 if c == `0` && s.pos > 5 && s.text[s.pos - 1] == `0` && s.text[s.pos - 2] == `x` && s.text[s.pos - 3] == `\\` { s.error('0 character in a string literal') } // ${var} if !s.is_fmt && c == `{` && prevc == `$` { s.inside_string = true // fmt.Println("breaking out of is()") // so that s.pos points to $ at the next step s.pos -= 2 // fmt.Println("break pos=", s.pos, "c=", string(s.text[s.pos]), "d=", s.text[s.pos]) break } // $var // if !s.is_fmt && c != `{` && c != ` ` && ! (c >= `0` && c <= `9`) && prevc == `$` { if !s.is_fmt && (c.is_letter() || c == `_`) && prevc == `$` { s.inside_string = true s.dollar_start = true // println('setting s.dollar=true pos=$s.pos') s.pos -= 2 break } } mut lit := '' if s.text[start] == SINGLE_QUOTE { start++ } mut end := s.pos if s.inside_string { end++ } if start > s.pos{} else { lit = s.text.substr(start, end) } // if lit.contains('\n') { // println('\nstring lit="$lit" pos=$s.pos line=$s.line_nr') // } /* for c in lit { if s.file_path.contains('range_test') { println('!') println(c) } } */ return lit } fn (s mut Scanner) ident_char() string { start := s.pos slash := `\\` mut len := 0 for { s.pos++ if s.pos >= s.text.len { break } if s.text[s.pos] != slash { len++ } double_slash := s.text[s.pos - 1] == slash && s.text[s.pos - 2] == slash if s.text[s.pos] == `\`` && (s.text[s.pos - 1] != slash || double_slash) { if double_slash { len++ } break } } len-- c := s.text.substr(start + 1, s.pos) if len != 1 { s.error('invalid character literal (more than one character: $len)') } return c } fn (p mut Parser) peek() Token { for { tok := p.scanner.peek() if tok != NL { return tok } } } fn (s mut Scanner) peek() Token { pos := s.pos line := s.line_nr inside_string := s.inside_string dollar_start := s.dollar_start dollar_end := s.dollar_end // ///// res := s.scan() tok := res.tok s.pos = pos s.line_nr = line s.inside_string = inside_string s.dollar_start = dollar_start s.dollar_end = dollar_end return tok } fn (s mut Scanner) debug_tokens() { s.pos = 0 fname := s.file_path.all_after('/') println('\n===DEBUG TOKENS $fname ============') // allToks := '' s.debug = true for { res := s.scan() tok := res.tok lit := res.lit // printiln(tok) print(tok.str()) // allToks += tok.String() if lit != '' { println(' `$lit`') // allToks += " `" + lit + "`" } else { println('') } // allToks += "\n" if tok == EOF { println('============ END OF DEBUG TOKENS ==================') // fmt.Println("========"+s.file+"========\n", allToks) break } } } fn is_name_char(c byte) bool { return c.is_letter() || c == `_` } fn (s mut Scanner) get_opening_bracket() int { mut pos := s.pos mut parentheses := 0 mut inside_string := false for pos > 0 && s.text[pos] != `\n` { if s.text[pos] == `)` && !inside_string { parentheses++ } if s.text[pos] == `(` && !inside_string { parentheses-- } if s.text[pos] == `\'` && s.text[pos - 1] != `\\` && s.text[pos - 1] != `\`` { inside_string = !inside_string } if parentheses == 0 { break } pos-- } return pos } // Foo { bar: 3, baz: 'hi' } => '{ bar: 3, baz: "hi" }' fn (s mut Scanner) create_type_string(T Type, name string) { line := s.line_nr inside_string := s.inside_string mut newtext := '\'{ ' start := s.get_opening_bracket() + 1 end := s.pos for i, field in T.fields { if i != 0 { newtext += ', ' } newtext += '$field.name: ' + '$${name}.${field.name}' } newtext += ' }\'' s.text = s.text.substr(0, start) + newtext + s.text.substr(end, s.text.len) s.pos = start - 2 s.line_nr = line s.inside_string = inside_string } fn (p mut Parser) create_type_string(T Type, name string) { p.scanner.create_type_string(T, name) }