///|
fn suffix(tokens : Array[Token], start : Int32) -> Array[Token] {
let out = []
for i in start.. Unit raise ConfigError {
let (p, cond) = conditional(tokens, start)
if p.length() != 1 || !p[0].quoted || (!linux612 && d.prompt != None) {
raise Invalid("expected one unique quoted prompt")
}
let had_prompt = d.prompt != None
d.prompt = Some(
if linux612 {
p[0].text.trim_start().to_owned()
} else {
p[0].text
},
)
d.prompt_if = if linux612 {
if had_prompt {
Disjunction(d.prompt_if, cond)
} else {
cond
}
} else {
Conjunction(d.prompt_if, cond)
}
}
///|
fn set_kind(
d : Definition,
kind : Kind,
linux612~ : Bool = false,
) -> Unit raise ConfigError {
match d.kind {
Some(k) =>
if k != kind && !linux612 {
raise Invalid("conflicting symbol type")
}
None => d.kind = Some(kind)
}
}
///|
fn property(
d : Definition,
ts : Array[Token],
linux612~ : Bool = false,
) -> Unit raise ConfigError {
match ts[0].text {
"bool" | "tristate" | "string" | "int" | "hex" => {
let kind = match ts[0].text {
"bool" => Boolean
"tristate" => Tristate
"string" => StringType
"int" => Integer
_ => Hex
}
set_kind(d, kind, linux612~)
if ts.length() > 1 {
set_prompt(d, ts, 1, linux612~)
}
}
"prompt" => set_prompt(d, ts, 1, linux612~)
"default" | "def_bool" | "def_tristate" => {
if ts[0].text == "def_bool" {
set_kind(d, Boolean, linux612~)
}
if ts[0].text == "def_tristate" {
set_kind(d, Tristate, linux612~)
}
let (v, c) = conditional(ts, 1)
d.defaults.push((parse_expression_tokens(v), c))
}
"depends" => {
if ts.length() < 3 || ts[1].text != "on" {
raise Invalid("expected depends on expression")
}
d.depends = Conjunction(d.depends, parse_expression_tokens(suffix(ts, 2)))
}
"select" | "imply" => {
let (names, cond) = conditional(ts, 1)
if names.length() != 1 || names[0].quoted || !identifier(names[0].text) {
raise Invalid("expected one target symbol")
}
if ts[0].text == "select" {
d.selects.push((names[0].text, cond))
} else {
d.implies.push((names[0].text, cond))
}
}
"option" | "modules" => {
if (
ts[0].text == "option" &&
(ts.length() != 2 || ts[1].text != "modules")
) ||
(ts[0].text == "modules" && ts.length() != 1) {
raise Invalid("only option modules is supported")
}
d.modules = true
}
"range" => {
let (bounds, cond) = conditional(ts, 1)
if bounds.length() != 2 {
raise Invalid("expected two range bounds")
}
d.ranges.push((atom(bounds[0]), atom(bounds[1]), cond))
}
_ => raise Invalid("unsupported property: " + ts[0].text)
}
}
///|
/// Parse the portable core; unsupported directives are errors, not ignored.
pub fn parse_kconfig(
text : String,
preprocessor~ : Preprocessor = Preprocessor::new(),
) -> Schema raise ConfigError {
if text.length() > 4194304 {
raise Invalid("Kconfig input exceeds 4 Mi UTF-16 units")
}
parse_sources("Kconfig", Map([("Kconfig", text)]), preprocessor~)
}
///|
priv struct Scope {
tag : String
choice_start : Int32
start : Int32
mut depends : Expr
mut visible : Expr
}
///|
fn close_scope(
schema : Schema,
scopes : Array[Scope],
expected : String,
) -> Unit raise ConfigError {
let scope = match scopes.pop() {
Some(s) => s
None => raise Invalid("unmatched " + expected)
}
if scope.tag != expected {
raise Invalid("mismatched scope terminator " + expected)
}
for i in scope.choice_start.. Schema raise ConfigError {
let schema = Schema::new(linux612~)
let scopes : Array[Scope] = []
let mut active_choice : Int32? = None
let mut current : Int32? = None
let mut comment_header = false
let mut seen_statement = false
let mut seen_modules = false
let mut pos = 0
while pos < lines.length() {
let line = lines[pos]
pos += 1
let ts = tokenize(line.text)
if ts.is_empty() {
continue
}
try {
if ts[0].quoted {
raise Invalid("quoted directive is invalid")
}
if [
"config", "menuconfig", "choice", "endchoice", "if", "endif", "menu", "endmenu",
"mainmenu", "comment",
].contains(ts[0].text) {
comment_header = false
}
if linux612 {
validate_linux_tokens(ts)
if ts[0].text == "modules" {
if seen_modules {
raise Invalid("modules directive repeated")
}
seen_modules = true
}
if ts[0].text == "mainmenu" && seen_statement {
raise Invalid("mainmenu must be the first statement")
}
if active_choice != None && ["menu", "menuconfig"].contains(ts[0].text) {
raise Invalid("statement not allowed inside choice")
}
if ts[0].text == "bool" &&
current == None &&
active_choice != None &&
ts.length() == 1 {
raise Invalid("choice bool requires a prompt")
}
}
seen_statement = true
match ts[0].text {
"config" | "menuconfig" => {
if ts.length() != 2 ||
ts[1].quoted ||
!identifier(ts[1].text) ||
(!linux612 && ["n", "m", "y"].contains(ts[1].text)) {
raise Invalid("invalid config declaration")
}
if schema.defs.length() >= 4096 {
raise Invalid("too many symbols")
}
match active_choice {
Some(i) => schema.choices[i].members.push(ts[1].text)
None => ()
}
let node = definition(ts[1].text, None, line.location)
node.choice_owner = match active_choice {
Some(i) => Some(schema.choices[i].name)
None => None
}
schema.defs.push(node)
current = Some(schema.defs.length() - 1)
}
"choice" => {
if linux612 && ts.length() != 1 {
raise Invalid("Linux v6.12 requires anonymous boolean choices")
}
if active_choice != None {
raise Invalid("nested choices unsupported")
}
if ts.length() > 2 ||
(ts.length() == 2 && (ts[1].quoted || !identifier(ts[1].text))) {
raise Invalid("invalid choice declaration")
}
let name = if ts.length() == 2 {
ts[1].text
} else {
"choice_" + schema.choices.length().to_string()
}
if schema.choices.any(fn(c) { c.name == name }) {
raise Invalid("duplicate choice name")
}
schema.choices.push({
name,
prompt: None,
prompt_if: truth(),
depends: truth(),
optional: false,
help: "",
defaults: [],
members: [],
})
active_choice = Some(schema.choices.length() - 1)
scopes.push({
tag: "endchoice",
start: schema.defs.length(),
choice_start: schema.choices.length(),
depends: truth(),
visible: truth(),
})
current = None
}
"endchoice" => {
if ts.length() != 1 {
raise Invalid("unexpected endchoice arguments")
}
let i = match active_choice {
Some(i) => i
None => raise Invalid("unmatched endchoice")
}
let c = schema.choices[i]
for
d in schema.defs.filter(fn(node) {
node.choice_owner == Some(c.name)
}) {
d.depends = Conjunction(d.depends, c.depends)
d.prompt_if = Conjunction(d.prompt_if, c.prompt_if)
if c.prompt == None {
d.prompt_if = Constant(Logic(N))
}
}
close_scope(schema, scopes, "endchoice")
active_choice = None
current = None
}
"if" | "menu" => {
if scopes.length() >= 64 {
raise Invalid("scope nesting exceeds 64")
}
let dep = if ts[0].text == "if" {
parse_expression_tokens(suffix(ts, 1))
} else {
if ts.length() != 2 || !ts[1].quoted {
raise Invalid("menu needs quoted title")
}
truth()
}
scopes.push({
tag: if ts[0].text == "if" {
"endif"
} else {
"endmenu"
},
choice_start: schema.choices.length(),
start: schema.defs.length(),
depends: dep,
visible: truth(),
})
current = None
}
"endif" | "endmenu" => {
if ts.length() != 1 {
raise Invalid("unexpected terminator arguments")
}
close_scope(schema, scopes, ts[0].text)
current = None
}
"mainmenu" | "comment" => {
comment_header = ts[0].text == "comment"
if ts.length() != 2 || !ts[1].quoted {
raise Invalid("expected quoted title")
}
current = None
}
"help" | "---help---" => {
if ts.length() != 1 {
raise Invalid("unexpected help arguments")
}
if current == None && active_choice == None {
raise Invalid("help outside symbol/choice")
}
let base = indentation(line.text)
let parts : Array[String] = []
let mut margin = -1
while pos < lines.length() {
let next = lines[pos].text
if !next.trim().is_empty() &&
(if linux612 {
indentation(next) < (if margin >= 0 { margin } else { 1 })
} else {
indentation(next) <= base
}) {
break
}
if !next.trim().is_empty() && margin < 0 {
margin = indentation(next)
}
if next.trim().is_empty() {
parts.push("")
} else {
let begin = if indentation(next) < margin {
indentation(next)
} else {
margin
}
parts.push(next[begin:].to_owned())
}
pos += 1
}
let help = parts.join("\n").trim().to_owned()
match current {
Some(i) => schema.defs[i].help = help
None =>
match active_choice {
Some(i) => schema.choices[i].help = help
None => ()
}
}
}
_ => {
if comment_header {
if ts[0].text != "depends" || ts.length() < 3 || ts[1].text != "on" {
raise Invalid("unsupported comment property")
}
ignore(parse_expression_tokens(suffix(ts, 2)))
continue
}
match current {
Some(i) => {
if linux612 && ts[0].text == "option" {
raise Invalid("option is not a Linux v6.12 directive")
}
property(schema.defs[i], ts, linux612~)
}
None => {
if active_choice is Some(i) &&
!scopes.is_empty() &&
scopes[scopes.length() - 1].tag == "endchoice" {
if linux612 && ts[0].text == "optional" {
raise Invalid("optional is not a Linux v6.12 directive")
}
choice_property(schema.choices[i], ts, linux612~)
continue
}
if scopes.is_empty() {
raise Invalid("property outside config/menu")
}
let scope = scopes[scopes.length() - 1]
if scope.tag != "endmenu" || scope.start != schema.defs.length() {
raise Invalid("property outside menu header")
}
if ts[0].text == "depends" &&
ts.length() > 2 &&
ts[1].text == "on" {
scope.depends = Conjunction(
scope.depends,
parse_expression_tokens(suffix(ts, 2)),
)
} else if ts[0].text == "visible" &&
ts.length() > 2 &&
ts[1].text == "if" {
scope.visible = Conjunction(
scope.visible,
parse_expression_tokens(suffix(ts, 2)),
)
} else {
raise Invalid("unsupported menu property")
}
}
}
}
}
} catch {
Invalid(msg) => raise Invalid(line.location + ": " + msg)
}
}
if !scopes.is_empty() {
raise Invalid("unterminated Kconfig scope")
}
let merged = merge_declarations(schema)
merged.validate()
merged
}