v/vlib/v/vmod/parser.v

272 lines
5.4 KiB
V

module vmod
import os
const err_label = 'vmod:'
enum TokenKind {
module_keyword
field_key
lcbr
rcbr
labr
rabr
comma
colon
eof
str
ident
unknown
}
pub struct Manifest {
pub mut:
name string
version string
description string
dependencies []string
license string
repo_url string
author string
unknown map[string][]string
}
struct Scanner {
mut:
pos int
line int = 1
text string
inside_text bool
tokens []Token
}
struct Parser {
mut:
file_path string
scanner Scanner
}
struct Token {
typ TokenKind
val string
line int
}
pub fn from_file(vmod_path string) ?Manifest {
if !os.exists(vmod_path) {
return error('v.mod: v.mod file not found.')
}
contents := os.read_file(vmod_path) or { '' }
return decode(contents)
}
pub fn decode(contents string) ?Manifest {
mut parser := Parser{
scanner: Scanner{
pos: 0
text: contents
}
}
return parser.parse()
}
fn (mut s Scanner) tokenize(t_type TokenKind, val string) {
s.tokens << Token{t_type, val, s.line}
}
fn (mut s Scanner) skip_whitespace() {
for s.pos < s.text.len && s.text[s.pos].is_space() {
s.pos++
}
}
fn is_name_alpha(chr u8) bool {
return chr.is_letter() || chr == `_`
}
fn (mut s Scanner) create_string(q u8) string {
mut str := ''
for s.pos < s.text.len && s.text[s.pos] != q {
if s.text[s.pos] == `\\` && s.text[s.pos + 1] == q {
str += s.text[s.pos..s.pos + 1]
s.pos += 2
} else {
str += s.text[s.pos].ascii_str()
s.pos++
}
}
return str
}
fn (mut s Scanner) create_ident() string {
mut text := ''
for s.pos < s.text.len && is_name_alpha(s.text[s.pos]) {
text += s.text[s.pos].ascii_str()
s.pos++
}
return text
}
fn (s Scanner) peek_char(c u8) bool {
return s.pos - 1 < s.text.len && s.text[s.pos - 1] == c
}
fn (mut s Scanner) scan_all() {
for s.pos < s.text.len {
c := s.text[s.pos]
if c.is_space() || c == `\\` {
s.pos++
if c == `\n` {
s.line++
}
continue
}
if is_name_alpha(c) {
name := s.create_ident()
if name == 'Module' {
s.tokenize(.module_keyword, name)
s.pos++
continue
} else if s.pos < s.text.len && s.text[s.pos] == `:` {
s.tokenize(.field_key, name + ':')
s.pos += 2
continue
} else {
s.tokenize(.ident, name)
s.pos++
continue
}
}
if c in [`'`, `\"`] && !s.peek_char(`\\`) {
s.pos++
str := s.create_string(c)
s.tokenize(.str, str)
s.pos++
continue
}
match c {
`{` { s.tokenize(.lcbr, c.ascii_str()) }
`}` { s.tokenize(.rcbr, c.ascii_str()) }
`[` { s.tokenize(.labr, c.ascii_str()) }
`]` { s.tokenize(.rabr, c.ascii_str()) }
`:` { s.tokenize(.colon, c.ascii_str()) }
`,` { s.tokenize(.comma, c.ascii_str()) }
else { s.tokenize(.unknown, c.ascii_str()) }
}
s.pos++
}
s.tokenize(.eof, 'eof')
}
fn get_array_content(tokens []Token, st_idx int) ?([]string, int) {
mut vals := []string{}
mut idx := st_idx
if tokens[idx].typ != .labr {
return error('$vmod.err_label not a valid array, at line ${tokens[idx].line}')
}
idx++
for {
tok := tokens[idx]
match tok.typ {
.str {
vals << tok.val
if tokens[idx + 1].typ !in [.comma, .rabr] {
return error('$vmod.err_label invalid separator "${tokens[idx + 1].val}", at line $tok.line')
}
idx += if tokens[idx + 1].typ == .comma { 2 } else { 1 }
}
.rabr {
idx++
break
}
else {
return error('$vmod.err_label invalid token "$tok.val", at line $tok.line')
}
}
}
return vals, idx
}
fn (mut p Parser) parse() ?Manifest {
if p.scanner.text.len == 0 {
return error('$vmod.err_label no content.')
}
p.scanner.scan_all()
tokens := p.scanner.tokens
mut mn := Manifest{}
if tokens[0].typ != .module_keyword {
return error('$vmod.err_label v.mod files should start with Module, at line ${tokens[0].line}')
}
mut i := 1
for i < tokens.len {
tok := tokens[i]
match tok.typ {
.lcbr {
if tokens[i + 1].typ !in [.field_key, .rcbr] {
return error('$vmod.err_label invalid content after opening brace, at line $tok.line')
}
i++
continue
}
.rcbr {
break
}
.field_key {
field_name := tok.val.trim_right(':')
if tokens[i + 1].typ !in [.str, .labr] {
return error('$vmod.err_label value of field "$field_name" must be either string or an array of strings, at line $tok.line')
}
field_value := tokens[i + 1].val
match field_name {
'name' {
mn.name = field_value
}
'version' {
mn.version = field_value
}
'license' {
mn.license = field_value
}
'repo_url' {
mn.repo_url = field_value
}
'description' {
mn.description = field_value
}
'author' {
mn.author = field_value
}
'dependencies' {
deps, idx := get_array_content(tokens, i + 1)?
mn.dependencies = deps
i = idx
continue
}
else {
if tokens[i + 1].typ == .labr {
vals, idx := get_array_content(tokens, i + 1)?
mn.unknown[field_name] = vals
i = idx
continue
}
mn.unknown[field_name] = [field_value]
}
}
i += 2
continue
}
.comma {
if tokens[i - 1].typ !in [.str, .rabr] || tokens[i + 1].typ != .field_key {
return error('$vmod.err_label invalid comma placement, at line $tok.line')
}
i++
continue
}
else {
return error('$vmod.err_label invalid token "$tok.val", at line $tok.line')
}
}
}
return mn
}