lix/code-generation: clean up code properly
The code within the code-generation had tons of code-duplicates and was overall quite meh to read, understand and expand. This commit refactors the code-generation to make it more readable and comprehensible, while also unifying its usage a bit more Change-Id: I3a5df8b7d8d9b6c76e02ef47dfb151c7dab7d0ab
This commit is contained in:
@@ -1,70 +1,102 @@
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from textwrap import dedent, indent
|
||||
from typing import NamedTuple
|
||||
|
||||
from frontmatter import Post
|
||||
|
||||
from common import cxx_literal, generate_file, load_data
|
||||
import argparse
|
||||
from common import cxx_literal, generate_file, load_data, get_argument_parser
|
||||
|
||||
KNOWN_KEYS = {"name", "type", "constructorArgs", "implementation", "impure", "renameInGlobalScope"}
|
||||
IMPURE_NOTE = """
|
||||
> **Note**
|
||||
>
|
||||
> Not available in [pure evaluation mode](@docroot@/command-ref/conf-file.md#conf-pure-eval).
|
||||
|
||||
"""
|
||||
|
||||
|
||||
class BuiltinConstant(NamedTuple):
|
||||
name: str
|
||||
type: str
|
||||
implementation: str
|
||||
impure: bool
|
||||
rename_in_global_scope: bool
|
||||
documentation: str
|
||||
class TypeName(NamedTuple):
|
||||
human: str
|
||||
code: str
|
||||
|
||||
|
||||
class BuiltinType(TypeName, Enum):
|
||||
attrs = TypeName("set", "nAttrs")
|
||||
boolean = TypeName("boolean", "nBool")
|
||||
integer = TypeName("integer", "nInt")
|
||||
list = TypeName("list", "nList")
|
||||
null = TypeName("null", "nNull")
|
||||
string = TypeName("string", "nString")
|
||||
|
||||
@classmethod
|
||||
def parse(cls, datum: Post) -> "BuiltinConstant":
|
||||
unknown_keys = set(datum.keys()) - KNOWN_KEYS
|
||||
if unknown_keys:
|
||||
msg = f"unknown keys: {unknown_keys!r}"
|
||||
raise ValueError(msg)
|
||||
if (constructor_args := datum.get("constructorArgs")) is not None:
|
||||
args = [f"NewValueAs::{datum['type']}"] + constructor_args # type: ignore
|
||||
impl = f"{{{','.join(args)}}}"
|
||||
else:
|
||||
impl = datum["implementation"]
|
||||
|
||||
return BuiltinConstant(
|
||||
name=datum["name"], # type: ignore
|
||||
type=datum["type"], # type: ignore
|
||||
implementation=impl,
|
||||
impure=datum.get("impure", False), # type: ignore
|
||||
rename_in_global_scope=datum.get("renameInGlobalScope", True), # type: ignore
|
||||
documentation=datum.content,
|
||||
)
|
||||
def from_string(cls, t_name: str) -> "BuiltinType":
|
||||
for t in cls:
|
||||
if t_name == t.name:
|
||||
return t
|
||||
msg = f"Invalid builtin type: {t_name}"
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
VALUE_TYPES = {
|
||||
"attrs": "nAttrs",
|
||||
"boolean": "nBool",
|
||||
"integer": "nInt",
|
||||
"list": "nList",
|
||||
"null": "nNull",
|
||||
"string": "nString",
|
||||
}
|
||||
@dataclasses.dataclass
|
||||
class BuiltinConstant:
|
||||
name: str
|
||||
documentation: str
|
||||
|
||||
HUMAN_TYPES = {
|
||||
"attrs": "set",
|
||||
"boolean": "Boolean",
|
||||
"integer": "integer",
|
||||
"list": "list",
|
||||
"null": "null",
|
||||
"string": "string",
|
||||
}
|
||||
# Fields with different name in the Post than in here
|
||||
# our fields
|
||||
type: BuiltinType = dataclasses.field(init=False)
|
||||
|
||||
# Post fields
|
||||
type_str: dataclasses.InitVar[str]
|
||||
constructor_args: dataclasses.InitVar[list[str] | None] = None
|
||||
|
||||
implementation: str = ""
|
||||
impure: bool = False
|
||||
rename_in_global_scope: bool = True
|
||||
|
||||
def __post_init__(self, type_str: str, constructor_args: list[str] | None):
|
||||
self.type = BuiltinType.from_string(type_str)
|
||||
if constructor_args is not None:
|
||||
args = [f"NewValueAs::{type_str}"] + constructor_args
|
||||
self.implementation = f"{{{','.join(args)}}}"
|
||||
|
||||
@property
|
||||
def code(self) -> str:
|
||||
cond = "if (!evalSettings.pureEval) " if self.impure else ""
|
||||
return dedent(f"""
|
||||
{cond} {{
|
||||
addConstant(
|
||||
{cxx_literal(("__" if self.rename_in_global_scope else "") + self.name)},
|
||||
{self.implementation},
|
||||
{{
|
||||
.type = {self.type.code},
|
||||
.doc = {cxx_literal(self.documentation)},
|
||||
.impureOnly = {cxx_literal(self.impure)},
|
||||
}}
|
||||
);
|
||||
}}
|
||||
""")
|
||||
|
||||
@property
|
||||
def docs(self) -> str:
|
||||
indentation = " " * 3
|
||||
return dedent(f"""
|
||||
<dt id="builtins-{self.name}">
|
||||
<a href="#builtins-{self.name}"><code>{self.name}</code></a> ({self.type.human})
|
||||
</dt>
|
||||
<dd>
|
||||
|
||||
{indent(self.documentation, indentation)}
|
||||
{indent(IMPURE_NOTE, indentation) if self.impure else ""}
|
||||
|
||||
</dd>
|
||||
|
||||
""")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--header", help="Path of the header to generate")
|
||||
ap.add_argument("--docs", help="Path of the documentation file to generate")
|
||||
ap.add_argument("defs", help="Builtin definition files", nargs="+")
|
||||
args = ap.parse_args()
|
||||
args = get_argument_parser().parse_args()
|
||||
|
||||
builtin_constants = load_data(args.defs, BuiltinConstant.parse)
|
||||
builtin_constants = load_data(args.defs, BuiltinConstant)
|
||||
|
||||
generate_file(
|
||||
args.header,
|
||||
@@ -72,40 +104,9 @@ def main():
|
||||
lambda constant:
|
||||
# `builtins` is magic and must come first
|
||||
"" if constant.name == "builtins" else constant.name,
|
||||
lambda constant: f"""{"if (!evalSettings.pureEval) " if constant.impure else ""}{{
|
||||
addConstant({cxx_literal(("__" if constant.rename_in_global_scope else "") + constant.name)}, {constant.implementation}, {{
|
||||
.type = {VALUE_TYPES[constant.type]},
|
||||
.doc = {cxx_literal(constant.documentation)},
|
||||
.impureOnly = {cxx_literal(constant.impure)},
|
||||
}});
|
||||
}}
|
||||
""",
|
||||
)
|
||||
generate_file(
|
||||
args.docs,
|
||||
builtin_constants,
|
||||
lambda constant: constant.name,
|
||||
lambda constant: f"""<dt id="builtins-{constant.name}">
|
||||
<a href="#builtins-{constant.name}"><code>{constant.name}</code></a> ({HUMAN_TYPES[constant.type]})
|
||||
</dt>
|
||||
<dd>
|
||||
|
||||
{constant.documentation}
|
||||
|
||||
"""
|
||||
+ (
|
||||
"""> **Note**
|
||||
>
|
||||
> Not available in [pure evaluation mode](@docroot@/command-ref/conf-file.md#conf-pure-eval).
|
||||
|
||||
"""
|
||||
if constant.impure
|
||||
else ""
|
||||
)
|
||||
+ """</dd>
|
||||
|
||||
""",
|
||||
lambda b: b.code,
|
||||
)
|
||||
generate_file(args.docs, builtin_constants, lambda constant: constant.name, lambda b: b.docs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,103 +1,89 @@
|
||||
from typing import NamedTuple
|
||||
import dataclasses
|
||||
from textwrap import dedent, indent
|
||||
|
||||
from frontmatter import Post
|
||||
|
||||
from build_experimental_features import ExperimentalFeature
|
||||
from common import cxx_literal, generate_file, load_data
|
||||
import argparse
|
||||
|
||||
KNOWN_KEYS = {"name", "implementation", "renameInGlobalScope", "args", "experimentalFeature"}
|
||||
from common import (
|
||||
cxx_literal,
|
||||
generate_file,
|
||||
load_data,
|
||||
get_argument_parser,
|
||||
get_experimental_features,
|
||||
)
|
||||
|
||||
|
||||
class Builtin(NamedTuple):
|
||||
@dataclasses.dataclass
|
||||
class Builtin:
|
||||
name: str
|
||||
implementation: str
|
||||
rename_in_global_scope: bool
|
||||
args: list[str]
|
||||
experimental_feature: str | None
|
||||
documentation: str
|
||||
args: list[str]
|
||||
experimental_feature: str | None = None
|
||||
implementation: str = ""
|
||||
rename_in_global_scope: bool = True
|
||||
|
||||
@classmethod
|
||||
def parse(cls, datum: Post) -> "Builtin":
|
||||
unknown_keys = set(datum.keys()) - KNOWN_KEYS
|
||||
if unknown_keys:
|
||||
msg = f"unknown keys: {unknown_keys!r}"
|
||||
raise ValueError(msg)
|
||||
return Builtin(
|
||||
name=datum["name"], # type: ignore
|
||||
implementation=datum["implementation"]
|
||||
if "implementation" in datum
|
||||
else f"prim_{datum['name']}",
|
||||
rename_in_global_scope=datum.get("renameInGlobalScope", True), # type: ignore
|
||||
args=datum["args"], # type: ignore
|
||||
experimental_feature=datum.get("experimentalFeature", None), # type: ignore
|
||||
documentation=datum.content,
|
||||
def __post_init__(self):
|
||||
self.implementation = self.implementation or f"prim_{self.name}"
|
||||
|
||||
def generate_code(self, experimental_features: dict[str, str]) -> str:
|
||||
xf = experimental_features[self.experimental_feature]
|
||||
cond = (
|
||||
f"if (experimentalFeatureSettings.isEnabled({xf})) "
|
||||
if self.experimental_feature
|
||||
else ""
|
||||
)
|
||||
return dedent(f"""
|
||||
{cond}{{
|
||||
addPrimOp({{
|
||||
.name = {cxx_literal(("__" if self.rename_in_global_scope else "") + self.name)},
|
||||
.args = {cxx_literal(self.args)},
|
||||
.arity = {len(self.args)},
|
||||
.doc = {cxx_literal(self.documentation)},
|
||||
.fun = {self.implementation},
|
||||
.experimentalFeature = {xf},
|
||||
}});
|
||||
}}
|
||||
""")
|
||||
|
||||
@property
|
||||
def docs(self) -> str:
|
||||
return dedent(f"""
|
||||
<dt id="builtins-{self.name}">
|
||||
<a href="#builtins-{self.name}"><code>{self.name} {
|
||||
" ".join([f"<var>{arg}</var>" for arg in self.args])
|
||||
}</code></a>
|
||||
</dt>
|
||||
<dd>
|
||||
|
||||
{indent(self.documentation, " " * 3)}
|
||||
|
||||
{
|
||||
f"This function is only available if the [{self.experimental_feature}](@docroot@/contributing/experimental-features.md#xp-feature-{self.experimental_feature}) experimental feature is enabled."
|
||||
if self.experimental_feature is not None
|
||||
else ""
|
||||
}
|
||||
</dd>
|
||||
|
||||
""")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--header", help="Path of the header to generate")
|
||||
ap.add_argument("--docs", help="Path of the documentation file to generate")
|
||||
ap = get_argument_parser()
|
||||
ap.add_argument(
|
||||
"--experimental-features", help="Directory containing the experimental feature definitions"
|
||||
)
|
||||
ap.add_argument("defs", help="Builtin definition files", nargs="+")
|
||||
args = ap.parse_args()
|
||||
|
||||
builtins = load_data(args.defs, Builtin.parse)
|
||||
builtins = load_data(args.defs, Builtin)
|
||||
|
||||
experimental_feature_names = {builtin.experimental_feature for (_, builtin) in builtins}
|
||||
experimental_feature_names.discard(None)
|
||||
experimental_feature_files = [
|
||||
f"{args.experimental_features}/{name}.md" for name in experimental_feature_names
|
||||
]
|
||||
experimental_features = load_data(experimental_feature_files, ExperimentalFeature.parse)
|
||||
experimental_features = {
|
||||
path_and_feature[1].name: f"Xp::{path_and_feature[1].internal_name}"
|
||||
for path_and_feature in experimental_features
|
||||
}
|
||||
experimental_features[None] = "std::nullopt"
|
||||
experimental_features = get_experimental_features(
|
||||
args.experimental_features, [b.experimental_feature for (_, b) in builtins]
|
||||
)
|
||||
|
||||
generate_file(
|
||||
args.header,
|
||||
builtins,
|
||||
lambda builtin: builtin.name,
|
||||
lambda builtin: f"""{"" if builtin.experimental_feature is None else f"if (experimentalFeatureSettings.isEnabled({experimental_features[builtin.experimental_feature]})) "}{{
|
||||
addPrimOp({{
|
||||
.name = {cxx_literal(("__" if builtin.rename_in_global_scope else "") + builtin.name)},
|
||||
.args = {cxx_literal(builtin.args)},
|
||||
.arity = {len(builtin.args)},
|
||||
.doc = {cxx_literal(builtin.documentation)},
|
||||
.fun = {builtin.implementation},
|
||||
.experimentalFeature = {experimental_features[builtin.experimental_feature]},
|
||||
}});
|
||||
}}
|
||||
""",
|
||||
)
|
||||
generate_file(
|
||||
args.docs,
|
||||
builtins,
|
||||
lambda builtin: builtin.name,
|
||||
lambda builtin: f"""<dt id="builtins-{builtin.name}">
|
||||
<a href="#builtins-{builtin.name}"><code>{builtin.name} {" ".join([f"<var>{arg}</var>" for arg in builtin.args])}</code></a>
|
||||
</dt>
|
||||
<dd>
|
||||
|
||||
{builtin.documentation}
|
||||
|
||||
"""
|
||||
+ (
|
||||
f"""This function is only available if the [{builtin.experimental_feature}](@docroot@/contributing/experimental-features.md#xp-feature-{builtin.experimental_feature}) experimental feature is enabled.
|
||||
|
||||
"""
|
||||
if builtin.experimental_feature is not None
|
||||
else ""
|
||||
)
|
||||
+ """</dd>
|
||||
|
||||
""",
|
||||
lambda b: b.generate_code(experimental_features),
|
||||
)
|
||||
generate_file(args.docs, builtins, lambda builtin: builtin.name, lambda b: b.docs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
from typing import NamedTuple
|
||||
|
||||
from frontmatter import Post
|
||||
|
||||
from common import cxx_literal, generate_file, load_data
|
||||
import argparse
|
||||
|
||||
KNOWN_KEYS = {"name", "internalName"}
|
||||
|
||||
|
||||
class ExperimentalFeature(NamedTuple):
|
||||
name: str
|
||||
internal_name: str
|
||||
description: str
|
||||
|
||||
@classmethod
|
||||
def parse(cls, datum: Post) -> "ExperimentalFeature":
|
||||
unknown_keys = set(datum.keys()) - KNOWN_KEYS
|
||||
if unknown_keys:
|
||||
msg = f"unknown keys: {unknown_keys!r}"
|
||||
raise ValueError(msg)
|
||||
return ExperimentalFeature(
|
||||
name=datum["name"], # type: ignore
|
||||
internal_name=datum["internalName"], # type: ignore
|
||||
description=datum.content,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--deprecated", action="store_true", help="Generate deprecated features")
|
||||
ap.add_argument("--header", help="Path of the declaration header to generate")
|
||||
ap.add_argument("--impl-header", help="Path of the implementation header to generate")
|
||||
ap.add_argument("--descriptions", help="Path of the description file to generate")
|
||||
ap.add_argument("--shortlist", help="Path of the shortlist file to generate")
|
||||
ap.add_argument("defs", help="Experimental feature definition files", nargs="+")
|
||||
args = ap.parse_args()
|
||||
|
||||
features = load_data(args.defs, ExperimentalFeature.parse)
|
||||
|
||||
generate_file(
|
||||
args.header,
|
||||
features,
|
||||
lambda feature: feature.name,
|
||||
lambda feature: f" {feature.internal_name},\n",
|
||||
)
|
||||
generate_file(
|
||||
args.impl_header,
|
||||
features,
|
||||
lambda feature: feature.name,
|
||||
lambda feature: f""" {{
|
||||
.tag = {"Dep" if args.deprecated else "Xp"}::{feature.internal_name},
|
||||
.name = {cxx_literal(feature.name)},
|
||||
.description = {cxx_literal(feature.description)},
|
||||
}},
|
||||
""",
|
||||
)
|
||||
generate_file(
|
||||
args.descriptions,
|
||||
features,
|
||||
lambda feature: feature.name,
|
||||
lambda feature: f"""## [`{feature.name}`]{{#{"dp" if args.deprecated else "xp"}-feature-{feature.name}}}
|
||||
|
||||
{feature.description}
|
||||
|
||||
""",
|
||||
)
|
||||
generate_file(
|
||||
args.shortlist,
|
||||
features,
|
||||
lambda feature: feature.name,
|
||||
lambda feature: f" - [`{feature.name}`](@docroot@/contributing/{'deprecated' if args.deprecated else 'experimental'}-features.md#{'dp' if args.deprecated else 'xp'}-feature-{feature.name})\n",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,74 @@
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from textwrap import dedent
|
||||
from typing import ClassVar, NamedTuple
|
||||
|
||||
from common import cxx_literal, generate_file, load_data, get_argument_parser
|
||||
|
||||
|
||||
class FeatureTypeNames(NamedTuple):
|
||||
code_tag: str
|
||||
doc_tag: str
|
||||
|
||||
|
||||
class FeatureType(FeatureTypeNames, Enum):
|
||||
experimental = FeatureTypeNames("Xp", "xp")
|
||||
deprecated = FeatureTypeNames("Dep", "dp")
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ExtraFeature:
|
||||
name: str
|
||||
internal_name: str
|
||||
documentation: str
|
||||
|
||||
type: ClassVar[FeatureType]
|
||||
|
||||
@property
|
||||
def code(self) -> str:
|
||||
return dedent(f"""
|
||||
{{
|
||||
.tag = {ExtraFeature.type.code_tag}::{self.internal_name},
|
||||
.name = {cxx_literal(self.name)},
|
||||
.description = {cxx_literal(self.documentation)},
|
||||
}},
|
||||
""")
|
||||
|
||||
@property
|
||||
def docs(self) -> str:
|
||||
return dedent(f"""
|
||||
## [`{self.name}`]{{#{ExtraFeature.type.doc_tag}-feature-{self.name}}}
|
||||
|
||||
{self.documentation}
|
||||
|
||||
""")
|
||||
|
||||
@property
|
||||
def short_docs(self) -> str:
|
||||
return f" - [`{self.name}`](@docroot@/contributing/{ExtraFeature.type.name}-features.md#{ExtraFeature.type.doc_tag}-feature-{self.name})\n"
|
||||
|
||||
|
||||
def main():
|
||||
ap = get_argument_parser()
|
||||
ap.add_argument("--deprecated", action="store_true", help="Generate deprecated features")
|
||||
ap.add_argument("--impl-header", help="Path of the implementation header to generate")
|
||||
ap.add_argument("--shortlist", help="Path of the shortlist file to generate")
|
||||
args = ap.parse_args()
|
||||
|
||||
ExtraFeature.type = FeatureType.deprecated if args.deprecated else FeatureType.experimental
|
||||
|
||||
features = load_data(args.defs, ExtraFeature)
|
||||
|
||||
generate_file(
|
||||
args.header,
|
||||
features,
|
||||
lambda feature: feature.name,
|
||||
lambda feature: f" {feature.internal_name},\n",
|
||||
)
|
||||
generate_file(args.impl_header, features, lambda feature: feature.name, lambda f: f.code)
|
||||
generate_file(args.docs, features, lambda feature: feature.name, lambda f: f.docs)
|
||||
generate_file(args.shortlist, features, lambda feature: feature.name, lambda f: f.short_docs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,64 +1,106 @@
|
||||
from typing import NamedTuple, Any
|
||||
import dataclasses
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
from frontmatter import Post
|
||||
|
||||
from build_experimental_features import ExperimentalFeature
|
||||
from common import cxx_literal, generate_file, load_data
|
||||
import argparse
|
||||
|
||||
KNOWN_KEYS = {
|
||||
"name",
|
||||
"internalName",
|
||||
"platforms",
|
||||
"type",
|
||||
"settingType",
|
||||
"default",
|
||||
"defaultExpr",
|
||||
"defaultText",
|
||||
"aliases",
|
||||
"experimentalFeature",
|
||||
"deprecated",
|
||||
}
|
||||
from common import (
|
||||
cxx_literal,
|
||||
generate_file,
|
||||
load_data,
|
||||
get_experimental_features,
|
||||
get_argument_parser,
|
||||
)
|
||||
|
||||
|
||||
class Setting(NamedTuple):
|
||||
PLATFORM_WARNING = """
|
||||
> **Note**
|
||||
> This setting is only available on {platforms} systems.
|
||||
|
||||
"""
|
||||
|
||||
XP_WARNING = """
|
||||
> **Warning**
|
||||
> This setting is part of an
|
||||
> [experimental feature](@docroot@/contributing/experimental-features.md).
|
||||
|
||||
To change this setting, you need to make sure the corresponding experimental feature,
|
||||
[`{feature}`](@docroot@/contributing/experimental-features.md#xp-feature-{feature}),
|
||||
is enabled.
|
||||
For example, include the following in [`nix.conf`](#):
|
||||
|
||||
```
|
||||
extra-experimental-features = {feature}
|
||||
{name} = ...
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
DEPR_WARNING = """
|
||||
> **Warning**
|
||||
> This setting is deprecated and will be removed in a future version of Lix.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class Setting:
|
||||
name: str
|
||||
internal_name: str
|
||||
description: str
|
||||
platforms: list[str] | None
|
||||
setting_type: str
|
||||
default_expr: str
|
||||
default_text: str
|
||||
aliases: list[str]
|
||||
experimental_feature: str | None
|
||||
deprecated: bool
|
||||
documentation: str
|
||||
|
||||
@classmethod
|
||||
def parse(cls, datum: Post) -> "Setting":
|
||||
unknown_keys = set(datum.keys()) - KNOWN_KEYS
|
||||
if unknown_keys:
|
||||
msg = f"unknown keys: {unknown_keys!r}"
|
||||
raise ValueError(msg)
|
||||
default_text = (
|
||||
f"`{nix_conf_literal(datum['default'])}`"
|
||||
if "default" in datum
|
||||
else datum["defaultText"]
|
||||
)
|
||||
if default_text == "``":
|
||||
default_text = "*empty*"
|
||||
return Setting(
|
||||
name=datum["name"], # type: ignore
|
||||
internal_name=datum["internalName"], # type: ignore
|
||||
description=datum.content,
|
||||
platforms=datum.get("platforms", None), # type: ignore
|
||||
setting_type=f"Setting<{datum['type']}>" if "type" in datum else datum["settingType"],
|
||||
default_expr=cxx_literal(datum["default"])
|
||||
if "default" in datum
|
||||
else datum["defaultExpr"],
|
||||
default_text=default_text,
|
||||
aliases=datum.get("aliases", []), # type: ignore
|
||||
experimental_feature=datum.get("experimentalFeature", None), # type: ignore
|
||||
deprecated=datum.get("deprecated", False), # type: ignore
|
||||
default_text: str = ""
|
||||
setting_type: str = ""
|
||||
default_expr: str = ""
|
||||
platforms: list[str] = dataclasses.field(default_factory=list)
|
||||
aliases: list[str] = dataclasses.field(default_factory=list)
|
||||
experimental_feature: str | None = None
|
||||
deprecated: bool = False
|
||||
|
||||
default: dataclasses.InitVar[str | None] = None
|
||||
type_str: dataclasses.InitVar[str | None] = None
|
||||
|
||||
def __post_init__(self, default: Any, type_str: str | None):
|
||||
if default is not None: # is not None nor an empty String
|
||||
self.default_text = f"`{nix_conf_literal(default)}`"
|
||||
self.default_expr = self.default_expr or cxx_literal(default)
|
||||
self.default_text = self.default_text or "*empty*"
|
||||
|
||||
if type_str is not None:
|
||||
self.setting_type = f"Setting<{type_str}>"
|
||||
|
||||
def generate_code(self, experimental_features: dict[str | None, str]) -> str:
|
||||
indentation = " " * 4
|
||||
expr = (indent(indentation, self.default_expr) + indentation) if "\n" in self.default_expr else self.default_expr
|
||||
return dedent(f"""
|
||||
{self.setting_type} {self.internal_name} {{
|
||||
this,
|
||||
{expr},
|
||||
{cxx_literal(self.name)},
|
||||
{cxx_literal(self.documentation)},
|
||||
{cxx_literal(self.aliases)},
|
||||
true,
|
||||
{experimental_features[self.experimental_feature]},
|
||||
{cxx_literal(self.deprecated)}
|
||||
}};
|
||||
""")
|
||||
|
||||
@property
|
||||
def docs(self) -> str:
|
||||
indentation = " " * 3
|
||||
platforms = [p.capitalize() for p in self.platforms]
|
||||
aliases = [f"`{item}`" for item in self.aliases]
|
||||
description = dedent(f"""
|
||||
|
||||
{indent(indentation, self.documentation)}
|
||||
|
||||
{indent(indentation, PLATFORM_WARNING.format(platforms=str(platforms)[1:-1])) if self.platforms else ""}
|
||||
{indent(indentation, XP_WARNING.format(feature=self.experimental_feature, name=self.name)) if self.experimental_feature is not None else ""}
|
||||
{indent(indentation, DEPR_WARNING) if self.deprecated else ""}
|
||||
**Default:** {self.default_text}
|
||||
{f"**Deprecated alias:** {str(aliases)[1:-1]}\n" if self.aliases else ""}
|
||||
""")
|
||||
return f'- <span id="conf-{self.name}">[`{self.name}`](#conf-{self.name})</span>' + indent(
|
||||
" ", # indent by two space to make it part of the list point
|
||||
description,
|
||||
)
|
||||
|
||||
|
||||
@@ -87,103 +129,28 @@ def indent(prefix: str, body: str) -> str:
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap = get_argument_parser()
|
||||
ap.add_argument("--kernel", help="Name of the kernel Lix will run on")
|
||||
ap.add_argument("--header", help="Path of the header to generate")
|
||||
ap.add_argument("--docs", help="Path of the documentation file to generate")
|
||||
ap.add_argument(
|
||||
"--experimental-features", help="Directory containing the experimental feature definitions"
|
||||
)
|
||||
ap.add_argument("defs", help="Setting definition files", nargs="+")
|
||||
args = ap.parse_args()
|
||||
|
||||
settings = load_data(args.defs, Setting.parse)
|
||||
settings = load_data(args.defs, Setting)
|
||||
|
||||
experimental_feature_names = {setting.experimental_feature for (_, setting) in settings}
|
||||
experimental_feature_names.discard(None)
|
||||
experimental_feature_files = [
|
||||
f"{args.experimental_features}/{name}.md" for name in experimental_feature_names
|
||||
]
|
||||
experimental_features = load_data(experimental_feature_files, ExperimentalFeature.parse)
|
||||
experimental_features = {
|
||||
path_and_feature[1].name: f"Xp::{path_and_feature[1].internal_name}"
|
||||
for path_and_feature in experimental_features
|
||||
}
|
||||
experimental_features[None] = "std::nullopt"
|
||||
experimental_features = get_experimental_features(
|
||||
args.experimental_features, [s.experimental_feature for (_, s) in settings]
|
||||
)
|
||||
|
||||
generate_file(
|
||||
args.header,
|
||||
settings,
|
||||
lambda setting: setting.name,
|
||||
lambda setting: f"""{setting.setting_type} {setting.internal_name} {{
|
||||
this,
|
||||
{setting.default_expr},
|
||||
{cxx_literal(setting.name)},
|
||||
{cxx_literal(setting.description)},
|
||||
{cxx_literal(setting.aliases)},
|
||||
true,
|
||||
{experimental_features[setting.experimental_feature]},
|
||||
{cxx_literal(setting.deprecated)}
|
||||
}};
|
||||
|
||||
"""
|
||||
if setting.platforms is None or args.kernel in setting.platforms
|
||||
lambda setting: setting.generate_code(experimental_features)
|
||||
if not setting.platforms or args.kernel in setting.platforms
|
||||
else "",
|
||||
)
|
||||
generate_file(
|
||||
args.docs,
|
||||
settings,
|
||||
lambda setting: setting.name,
|
||||
lambda setting: f"""- <span id="conf-{setting.name}">[`{setting.name}`](#conf-{setting.name})</span>
|
||||
|
||||
{indent(" ", setting.description)}
|
||||
"""
|
||||
+ (
|
||||
f""" > **Note**
|
||||
> This setting is only available on {", ".join([platform_names[platform] for platform in setting.platforms])} systems.
|
||||
|
||||
"""
|
||||
if setting.platforms is not None
|
||||
else ""
|
||||
)
|
||||
+ (
|
||||
f""" > **Warning**
|
||||
> This setting is part of an
|
||||
> [experimental feature](@docroot@/contributing/experimental-features.md).
|
||||
|
||||
To change this setting, you need to make sure the corresponding experimental feature,
|
||||
[`{setting.experimental_feature}`](@docroot@/contributing/experimental-features.md#xp-feature-{setting.experimental_feature}),
|
||||
is enabled.
|
||||
For example, include the following in [`nix.conf`](#):
|
||||
|
||||
```
|
||||
extra-experimental-features = {setting.experimental_feature}
|
||||
{setting.name} = ...
|
||||
```
|
||||
|
||||
"""
|
||||
if setting.experimental_feature is not None
|
||||
else ""
|
||||
)
|
||||
+ (
|
||||
""" > **Warning**
|
||||
> This setting is deprecated and will be removed in a future version of Lix.
|
||||
|
||||
"""
|
||||
if setting.deprecated
|
||||
else ""
|
||||
)
|
||||
+ f""" **Default:** {setting.default_text}
|
||||
|
||||
"""
|
||||
+ (
|
||||
f""" **Deprecated alias:** {", ".join([f"`{item}`" for item in setting.aliases])}
|
||||
|
||||
"""
|
||||
if setting.aliases != []
|
||||
else ""
|
||||
),
|
||||
)
|
||||
generate_file(args.docs, settings, lambda setting: setting.name, lambda setting: setting.docs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import argparse
|
||||
import re
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -42,23 +44,52 @@ def cxx_literal(v: Any) -> str:
|
||||
raise NotImplementedError(msg)
|
||||
|
||||
|
||||
def load_data[T](defs: list[str], parse_function: Callable[[frontmatter.Post], T]) -> list[T]:
|
||||
def get_experimental_features(
|
||||
base_path: str, human_names: list[str | None]
|
||||
) -> dict[str | None, str]:
|
||||
experimental_feature_files = {
|
||||
f"{base_path}/{xp_name}.md" for xp_name in human_names if xp_name is not None
|
||||
}
|
||||
|
||||
from build_extra_features import ExtraFeature # noqa: PLC0415 # Avoid cyclic import
|
||||
|
||||
experimental_features_data = load_data(list(experimental_feature_files), ExtraFeature)
|
||||
experimental_features: dict[str | None, str] = {
|
||||
xf.name: f"Xp::{xf.internal_name}" for _, xf in experimental_features_data
|
||||
}
|
||||
experimental_features[None] = "std::nullopt"
|
||||
return experimental_features
|
||||
|
||||
|
||||
FIELD_RENAMES = {"type": "type_str", "content": "documentation"}
|
||||
|
||||
|
||||
def load_data[T](defs: list[str], parse_function: type[T]) -> list[tuple[str, T]]:
|
||||
data = []
|
||||
for path in defs:
|
||||
try:
|
||||
datum = frontmatter.load(path)
|
||||
data.append((path, parse_function(datum)))
|
||||
datum = {
|
||||
# convert camelCase to snake_case
|
||||
re.sub(r"(?<=.)([A-Z])", lambda m: f"_{m.group(1).lower()}", k): v
|
||||
for k, v in frontmatter.load(path).to_dict().items()
|
||||
}
|
||||
|
||||
for post_name, field_name in FIELD_RENAMES.items():
|
||||
if post_name in datum:
|
||||
datum[field_name] = datum.pop(post_name)
|
||||
|
||||
data.append((path, parse_function(**datum)))
|
||||
except Exception as e:
|
||||
e.add_note(f"in {path}")
|
||||
raise
|
||||
return data
|
||||
|
||||
|
||||
def generate_file(
|
||||
def generate_file[T](
|
||||
path: str | None,
|
||||
data: list[Any],
|
||||
sort_key_function: Callable[[Any], str],
|
||||
generate_function: Callable[[Any], str],
|
||||
data: list[T],
|
||||
sort_key_function: Callable[[T], str],
|
||||
generate_function: Callable[[T], str],
|
||||
):
|
||||
if path is not None:
|
||||
with Path(path).open("w") as out:
|
||||
@@ -66,7 +97,17 @@ def generate_file(
|
||||
data, key=lambda path_and_datum: sort_key_function(path_and_datum[1])
|
||||
):
|
||||
try:
|
||||
out.write(generate_function(datum))
|
||||
text = generate_function(datum)
|
||||
out.write(text)
|
||||
except Exception as e:
|
||||
e.add_note(f"in {path}")
|
||||
raise
|
||||
|
||||
|
||||
def get_argument_parser() -> argparse.ArgumentParser:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--header", help="Path of the header to generate")
|
||||
ap.add_argument("--docs", help="Path of the documentation file to generate")
|
||||
ap.add_argument("defs", help="Builtin definition files", nargs="+")
|
||||
|
||||
return ap
|
||||
|
||||
@@ -187,11 +187,11 @@ deprecated_feature_definitions = files(
|
||||
experimental_features_gen = custom_target(
|
||||
command : [
|
||||
python.full_path(),
|
||||
'@SOURCE_ROOT@/lix/code-generation/build_experimental_features.py',
|
||||
'@SOURCE_ROOT@/lix/code-generation/build_extra_features.py',
|
||||
'--header', '@OUTPUT0@',
|
||||
'--impl-header', '@OUTPUT1@',
|
||||
'--shortlist', '@OUTPUT2@',
|
||||
'--descriptions', '@OUTPUT3@',
|
||||
'--docs', '@OUTPUT3@',
|
||||
'@INPUT@',
|
||||
],
|
||||
input : experimental_feature_definitions,
|
||||
@@ -219,12 +219,12 @@ experimental_feature_descriptions_md = experimental_features_gen[3]
|
||||
deprecated_features_gen = custom_target(
|
||||
command : [
|
||||
python.full_path(),
|
||||
'@SOURCE_ROOT@/lix/code-generation/build_experimental_features.py',
|
||||
'@SOURCE_ROOT@/lix/code-generation/build_extra_features.py',
|
||||
'--deprecated',
|
||||
'--header', '@OUTPUT0@',
|
||||
'--impl-header', '@OUTPUT1@',
|
||||
'--shortlist', '@OUTPUT2@',
|
||||
'--descriptions', '@OUTPUT3@',
|
||||
'--docs', '@OUTPUT3@',
|
||||
'@INPUT@',
|
||||
],
|
||||
input : deprecated_feature_definitions,
|
||||
|
||||
Reference in New Issue
Block a user