add validator to restrict the flexibility of LConfig
authorLunaixsky <lunaixsky@qq.com>
Sat, 10 May 2025 00:47:51 +0000 (01:47 +0100)
committerLunaixsky <lunaixsky@qq.com>
Sat, 10 May 2025 00:47:51 +0000 (01:47 +0100)
* allow a bool config option change value based on other option's value
  similar to "select" in kconfig, but it is distributed to the actual
  affecting flags rather than centered around the master option

17 files changed:
lunaix-os/LConfig
lunaix-os/arch/LConfig
lunaix-os/arch/x86/LConfig
lunaix-os/hal/LConfig
lunaix-os/hal/bus/LConfig
lunaix-os/hal/char/uart/LConfig
lunaix-os/kernel/mm/LConfig
lunaix-os/scripts/build-tools/lcfg2/ast_validator.py [new file with mode: 0644]
lunaix-os/scripts/build-tools/lcfg2/common.py
lunaix-os/scripts/build-tools/lcfg2/lazy.py
lunaix-os/scripts/build-tools/lcfg2/nodes.py
lunaix-os/scripts/build-tools/lcfg2/rewriter.py
lunaix-os/scripts/build-tools/lcfg2/rules.py [new file with mode: 0644]
lunaix-os/scripts/build-tools/lcfg2/sanitiser.py
lunaix-os/scripts/build-tools/lib/utils.py
lunaix-os/scripts/build-tools/luna_build.py
lunaix-os/scripts/build-tools/shared/shconfig/commands.py

index d66375dbb27fa1da91e1d7c7bbb935ff1c91ec63..56cc436361714c787f59f195675a4df2db5ec63c 100644 (file)
@@ -2,6 +2,15 @@ from datetime import datetime, date
 
 from . import kernel, arch, hal
 
+@native
+def get_patch_seq():
+    today = date.today()
+    year = today.year
+    start_of_year = datetime(year, 1, 1).date()
+    seq_num = (today - start_of_year).days
+    
+    return "%d%d"%(year - 2000, seq_num)
+
 @"Kernel Version"
 @readonly
 def lunaix_ver() -> str:
@@ -9,12 +18,7 @@ def lunaix_ver() -> str:
     Lunaix kernel version
     """
     
-    today = date.today()
-    year = today.year
-    start_of_year = datetime(year, 1, 1).date()
-    seq_num = (today - start_of_year).days
-    
-    return "%s v0.%d%d"%(arch.val, year - 2000, seq_num)
+    return f"{arch.val} v0.0.{get_patch_seq()}"
 
 @"Kernel Debug and Testing"
 def debug_and_testing():
index 78d557a3f856dd691a4f3310e4828fa228c0be4b..bb22a10bc6e42b247e872c263373c8a97855494b 100644 (file)
@@ -8,15 +8,16 @@ def architecture_support():
 
     @flag
     def arch_x86_32() -> bool:
-        return arch.val == "i386"
+        when(arch is "i386")
     
     @flag
     def arch_x86_64() -> bool:
-        return arch.val == "x86_64"
+        when(arch is "x86_64")
     
     @flag
     def arch_x86() -> bool:
-        return arch.val in ["x86_64", "i386"]
+        when(arch is "i386")
+        when(arch is "x86_64")
 
     @"Architecture"
     def arch() -> "i386" | "x86_64":
index e5f0cd40fd302a613913c3dde213b577e1705c66..0e4d46382e8cc85af95df950e9869303159cbf04 100644 (file)
@@ -6,11 +6,11 @@ def x86_configurations():
 
     @flag
     def x86_bl_mb() -> bool:
-        return x86_bl.val == "mb"
+        when (x86_bl is "mb")
     
     @flag
     def x86_bl_mb2() -> bool:
-        return x86_bl.val == "mb2"
+        when (x86_bl is "mb2")
     
     @"Use SSE2/3/4 extension"
     def x86_enable_sse_feature() -> bool:
@@ -18,7 +18,7 @@ def x86_configurations():
             Config whether to allow using SSE feature for certain
             optimization
         """
-        
+
         return False
 
     @"Bootloader Model"
index 26f53b8815862704ee5aa5b504173128f0af331d..b2e00fa77c7b659a6147b79aa9dddde55b98658e 100644 (file)
@@ -16,8 +16,9 @@ def hal():
             devicetree might be mandatory and perhaps the only
             way.
         """
+        require(not arch_x86)
 
-        return arch.val not in ["x86_64", "i386"]
+        return False
 
     @"Maximum size of device tree blob (in KiB)"
     @readonly
index edbe2ef0608719ee9ac29fc30a4b0fdbf7b4387b..769072a3cc964a4c05b9ba255e543b4a6788ff4d 100644 (file)
@@ -22,4 +22,4 @@ def bus_if():
         require(not pcie_ext and pci_enable)
         require(arch_x86)
 
-        return arch.val in [ "i386", "x86_64" ]
+        return True
index 831169cfb119c6637ad4fa7f26d427ba0f3755db..10a74f51ab2c84674d545e585bf9a0996d51dc09 100644 (file)
@@ -7,8 +7,9 @@ def uart_16x50():
     @"16x50 XT-Compat"
     def xt_16x50() -> bool:
         """ Enable the 16x50 for PC-compatible platform  """
-
-        return arch.val in ["i386", "x86_64"]
+        require(arch_x86)
+        
+        return True
     
     @"16x50 PCI"
     def pci_16x50() -> bool:
index 8f8d4b9f9c8da1308f5f8cfe7352ffacdce64749..fa7c431055201217f09511e6b60ed354f802a5f9 100644 (file)
@@ -10,15 +10,15 @@ def memory_subsystem():
 
         @flag
         def pmalloc_method_simple() -> bool:
-            return pmalloc_method.val == "simple"
+            when (pmalloc_method is "simple")
         
         @flag
         def pmalloc_method_buddy() -> bool:
-            return pmalloc_method.val == "buddy"
+            when (pmalloc_method is "buddy")
         
         @flag
         def pmalloc_method_ncontig() -> bool:
-            return pmalloc_method.val == "ncontig"
+            when (pmalloc_method is "ncontig")
 
         @"Allocation policy"
         def pmalloc_method() -> "simple" | "buddy" | "ncontig":
@@ -29,7 +29,7 @@ def memory_subsystem():
         @"PMalloc Thresholds"
         def pmalloc_simple_po_thresholds():
 
-            require(pmalloc_method_simple)
+            require (pmalloc_method_simple)
             
             @"Maximum cached order-0 free pages"
             def pmalloc_simple_max_po0() -> int:
diff --git a/lunaix-os/scripts/build-tools/lcfg2/ast_validator.py b/lunaix-os/scripts/build-tools/lcfg2/ast_validator.py
new file mode 100644 (file)
index 0000000..2b63e31
--- /dev/null
@@ -0,0 +1,71 @@
+import ast
+import inspect
+import textwrap
+
+from typing import Callable
+from lib.utils import Schema, ConfigASTVisitor, SourceLogger
+from .common import Schema, ConfigNodeError
+
+class Rule:
+    def __init__(self, t, v, name, fn):
+        self.type = t
+        self.__name = name
+        self.__var = v
+        self.__fn = fn
+        self.__help_msg = inspect.getdoc(fn)
+        self.__help_msg = textwrap.dedent(self.__help_msg.strip())
+
+    def match_variant(self, astn):
+        if not self.__var:
+            return True
+        return self.__var.match(astn)
+    
+    def invoke(self, reducer, node):
+        if self.__fn(reducer._rules, reducer, node):
+           return
+
+        SourceLogger.warn(reducer._cfgn, node, 
+                          f"rule violation: {self.__name}: {self.__help_msg}")
+        # raise ConfigNodeError(reducer._cfgn, 
+        #         f"rule failed: {self.__name}: {self.__help_msg}")
+
+def rule(ast_type: type, variant: Schema, name: str):
+    def __rule(fn: Callable):
+        return Rule(ast_type, variant, name, fn)
+    return __rule
+
+class RuleCollection:
+    def __init__(self):
+        self.__rules = {}
+
+        members = inspect.getmembers(self, lambda p: isinstance(p, Rule))
+        for _, rule in members:
+            t = rule.type
+            if rule.type not in self.__rules:
+                self.__rules[t] = [rule]
+            else:
+                self.__rules[t].append(rule)
+    
+    def execute(self, reducer, node):
+        rules = self.__rules.get(type(node))
+        if not rules:
+            return
+        
+        for rule in rules:
+            if not rule.match_variant(node):
+                continue
+            rule.invoke(reducer, node)
+
+class NodeValidator(ast.NodeTransformer):
+    def __init__(self, all_rules):
+        super().__init__()
+        self._rules = all_rules
+
+    def validate(self, cfgn, astn):
+        self._cfgn = cfgn
+        self.visit(astn)
+
+    def visit(self, node):
+        self._rules.execute(self, node)
+        return super().visit(node)
index 94cdcea201bfb4a2ccd8c4b91229fdf1d64fdfd2..21e75b166ddf5dc75e4a2f369518e331dbf2bbb5 100644 (file)
@@ -19,6 +19,7 @@ class NodeProperty:
     Enabled     = PropertyAccessor("$enabled")
     Status      = PropertyAccessor("$status")
     Dependency  = PropertyAccessor("$depends")
+    WhenToggle  = PropertyAccessor("$when")
     Hidden      = PropertyAccessor("hidden")
     Parent      = PropertyAccessor("parent")
     Label       = PropertyAccessor("label")
index ddc02edad052be0a5eb1940b88c3efac0cff10be..1c634fd7a2006612b1812af596d0c42e642993f6 100644 (file)
@@ -84,9 +84,14 @@ class Lazy:
         
         type_ = astn.attr
         target = astn.value.id
+       
+        return Lazy.from_type(cfgnode, type_, target)
+    
+    @staticmethod
+    def from_type(cfgnode, type_, target):   
         key = Lazy.get_key_from(type_, target)
-
         lz = cfgnode._lazy_table.get(key)
+        
         if lz:
             return key
 
@@ -94,3 +99,4 @@ class Lazy:
         cfgnode._lazy_table.put(lz)
 
         return key
+
index 703b8f96a6a25c5bfa7238c8706cc827b9abff45..19ead504bb9cf91402f4be5c4295fdbcc35ac1ce 100644 (file)
@@ -5,6 +5,10 @@ from .common     import NodeProperty, ConfigNodeError, NodeDependency
 from .lazy       import LazyLookup
 from .rewriter   import ConfigNodeASTRewriter
 
+from .ast_validator import NodeValidator
+from .rules import SyntaxRule
+
+validator = NodeValidator(SyntaxRule())
 
 class ConfigDecorator:
     Label       = Schema(ast.Constant)
@@ -37,7 +41,10 @@ class ConfigNode:
         NodeProperty.Status[self]   = "Empty"
 
     def set_node_body(self, ast_nodes, rewriter = ConfigNodeASTRewriter):
-        new_ast = rewriter(self).visit(ast.Module(ast_nodes))
+        new_ast = ast.Module(ast_nodes, [])
+        validator.validate(self, new_ast)
+        
+        new_ast = rewriter(self).rewrite(new_ast)
         NodeDependency.try_create(self)
 
         fn_name = f"__fn_{self._name}"
index 9f47ad35af10190542c35b70f3712054a0fb8e0e..d2003eb8d4c8ef637667bbd403b838741c6e047b 100644 (file)
@@ -4,29 +4,46 @@ from lib.utils  import Schema
 from .lazy       import Lazy
 from .common     import NodeProperty
 
+class RewriteRule:
+    MaybeBuiltin = Schema(
+                        ast.Call, 
+                            func=Schema(ast.Name),
+                            args=[ast.expr])
+
+    WhenTogglerItem = Schema(
+                            ast.Compare,
+                                left=ast.Name,
+                                ops=[Schema.Union(ast.Is, ast.IsNot)],
+                                comparators=[ast.Constant])
+    
+    WhenToggler = Schema(
+                    Schema.Union(
+                        WhenTogglerItem,
+                        Schema(ast.BoolOp, 
+                            op=ast.And, 
+                            values=Schema.List(WhenTogglerItem))))
+
 class ConfigNodeASTRewriter(ast.NodeTransformer):
-    Depend = Schema(
-                ast.Call, 
-                    func=Schema(ast.Name, id='require'),
-                    args=[ast.expr])
+    
 
     def __init__(self, cfg_node):
         super().__init__()
 
         self.__cfg_node = cfg_node
 
+    def __subscript_accessor(self, name, ctx, token):
+        return ast.Subscript(
+            value=ast.Name("__lzLut__", ctx=ast.Load()),
+            slice=ast.Constant(name),
+            ctx=ctx,
+        )
+
     def __gen_accessor(self, orig):
         key = Lazy.from_astn(self.__cfg_node, orig)
         if not key:
             return self.generic_visit(orig)
         
-        return ast.Subscript(
-            value=ast.Name("__lzLut__", ctx=ast.Load()),
-            slice=ast.Constant(key),
-            ctx=orig.ctx,
-            lineno=orig.lineno,
-            col_offset=orig.col_offset
-        )
+        return self.__subscript_accessor(key, orig.ctx, orig)
     
     def __gen_dependency(self, node):
         cfgn = self.__cfg_node
@@ -42,20 +59,83 @@ class ConfigNodeASTRewriter(ast.NodeTransformer):
         dep_expr = ast.BoolOp(ast.And(), [dep_expr, node])
         NodeProperty.Dependency[cfgn] = dep_expr
 
+    def __gen_when_expr(self, node):
+        and_list = []
+        cfgn = self.__cfg_node
+
+        if RewriteRule.WhenToggler != node:
+            raise cfgn.config_error(
+                f"invalid when(...) expression: {ast.unparse(node)}")
+
+        if RewriteRule.WhenTogglerItem == node:
+            and_list.append(node)
+        else:
+            and_list += node.values
+        
+        for i in range(len(and_list)):
+            item = and_list[i]
+            operator = item.ops[0]
+            
+            name = Lazy.from_type(cfgn, Lazy.NodeValue, item.left.id)
+            acc = self.__subscript_accessor(name, ast.Load(), node)
+            
+            if isinstance(operator, ast.Is):
+                operator = ast.Eq()
+            else:
+                operator = ast.NotEq()
+            
+            item.left = acc
+            item.ops  = [operator]
+            
+        
+        current = ast.BoolOp(
+                        op=ast.And(), 
+                        values=[ast.Constant(True), *and_list])
+        
+        expr = NodeProperty.WhenToggle[cfgn]
+        if expr:
+            assert isinstance(expr, ast.expr)
+            current = ast.BoolOp(op=ast.Or(), values=[expr, current])
+
+        NodeProperty.WhenToggle[cfgn] = current
+
     def visit_Attribute(self, node):
         return self.__gen_accessor(node)
     
     def visit_Expr(self, node):
         val = node.value
         
-        if ConfigNodeASTRewriter.Depend != val:
+        if RewriteRule.MaybeBuiltin != val:
             return self.generic_visit(node)
         
         # Process marker functions
         name = val.func.id
         if name == "require":
             self.__gen_dependency(val.args[0])
+        elif name == "when":
+            self.__gen_when_expr(val.args[0])
         else:
             return self.generic_visit(node)
         
         return None
+    
+    def visit_Return(self, node):
+        if NodeProperty.WhenToggle[self.__cfg_node]:
+            return None
+        return self.generic_visit(node)
+    
+    def visit_Is(self, node):
+        return ast.Eq()
+    
+    def rewrite(self, node):
+        assert isinstance(node, ast.Module)
+        node = self.visit(node)
+
+        expr = NodeProperty.WhenToggle[self.__cfg_node]
+        if not expr:
+            return node
+        
+        del NodeProperty.WhenToggle[self.__cfg_node]
+        
+        node.body.append(ast.Return(expr, lineno=0, col_offset=0))
+        return node
diff --git a/lunaix-os/scripts/build-tools/lcfg2/rules.py b/lunaix-os/scripts/build-tools/lcfg2/rules.py
new file mode 100644 (file)
index 0000000..35f770f
--- /dev/null
@@ -0,0 +1,96 @@
+import ast
+
+from .ast_validator import RuleCollection, rule
+from lib.utils import Schema
+
+class SyntaxRule(RuleCollection):
+    NodeAssigment = Schema(ast.Subscript, 
+                            value=Schema(ast.Name, id='__lzLut__'), 
+                            ctx=ast.Store)
+    TrivialValue = Schema(Schema.Union(
+        ast.Constant, 
+        ast.Name, 
+        Schema(ast.Subscript, 
+               value=Schema(ast.Name, id='__lzLut__'), 
+               slice=ast.Constant)
+    ))
+
+    BoolOperators = Schema(Schema.Union(ast.Or, ast.And))
+    
+    TrivialTest    = Schema(ast.Compare, 
+                          left=TrivialValue, 
+                          ops=[Schema.Union(ast.Eq, ast.In)],
+                          comparators=[Schema.Union(
+                              ast.Constant, 
+                              Schema(ast.List, elts=Schema.List(ast.Constant))
+                          )])
+    
+    InlineIf       = Schema(ast.IfExp, 
+                            test=Schema.Union(TrivialTest, TrivialValue), 
+                            body=TrivialValue, 
+                            orelse=TrivialValue)
+    
+    TrivialLogic   = Schema(ast.BoolOp, 
+                            op=BoolOperators, 
+                            values=Schema.List(
+                                Schema.Union(TrivialTest, ast.Name)
+                            ))
+    
+    TrivialReturn  = Schema(Schema.Union(
+        TrivialValue,
+        InlineIf,
+        TrivialLogic,
+        ast.JoinedStr
+    ))
+
+    def __init__(self):
+        super().__init__()
+    
+    @rule(ast.If, None, "dynamic-logic")
+    def __dynamic_logic(self, reducer, node):
+        """
+        Conditional branching could interfering dependency resolving
+        """
+        return False
+    
+    @rule(ast.While, None, "while-loop")
+    def __while_loop(self, reducer, node):
+        """
+        loop construct may impact with readability.
+        """
+        return False
+    
+    @rule(ast.For, None, "for-loop")
+    def __for_loop(self, reducer, node):
+        """
+        loop construct may impact with readability.
+        """
+        return False
+    
+    @rule(ast.ClassDef, None, "class-def")
+    def __class_definition(self, reducer, node):
+        """
+        use of custom class is not recommended
+        """
+        return False
+    
+    @rule(ast.Dict, None, "complex-struct")
+    def __complex_datastruct(self, reducer, node):
+        """
+        use of complex data structure is not recommended
+        """
+        return False
+    
+    @rule(ast.Subscript, NodeAssigment, "side-effect-option")
+    def __side_effect(self, reducer, node):
+        """
+        Option modifying other options dynamically unpredictable behaviour
+        """
+        return False
+    
+    @rule(ast.Return, None, "non-trivial-value")
+    def __nontrivial_return(self, reducer, node):
+        """
+        Option default should be kept as constant or simple membership check
+        """
+        return SyntaxRule.TrivialReturn == node.value 
\ No newline at end of file
index 9a329719599aa059465a5cf7dee621a265a0830c..1c84a77bf0759c14b466cd021d6ce3427aa536a3 100644 (file)
@@ -1,7 +1,6 @@
 import ast
 
 from lib.utils import Schema, ConfigASTVisitor, SourceLogger
-from .common import NodeProperty
 
 class TreeSanitiser(ConfigASTVisitor):
     DecoNative = Schema(ast.Name, id="native")
index 9d85da5dcb89e4fe7736d15da1beb79bce10b018..6dd1ace18baf5700259c289982274b460708e57b 100644 (file)
@@ -1,5 +1,6 @@
 import os, ast
 from pathlib import Path
+from typing import Any, List
 
 def join_path(stem, path):
     if os.path.isabs(path):
@@ -14,6 +15,9 @@ class Schema:
 
         def __str__(self):
             return "Any"
+        
+        def __repr__(self):
+            return self.__str__()
 
     class Union:
         def __init__(self, *args):
@@ -22,6 +26,20 @@ class Schema:
         def __str__(self):
             strs = [Schema.get_type_str(t) for t in self.union]
             return f"{' | '.join(strs)}"
+        
+        def __repr__(self):
+            return self.__str__()
+        
+    class List:
+        def __init__(self, el_type):
+            self.el_type = el_type
+        
+        def __str__(self):
+            strs = Schema.get_type_str(self.el_type)
+            return f"*{strs}"
+        
+        def __repr__(self):
+            return self.__str__()
 
     def __init__(self, type, **kwargs):
         self.__type = type
@@ -37,6 +55,16 @@ class Schema:
             
         return True
     
+    def __match_list_member(self, actual, expect):
+        if not isinstance(actual, List):
+            return False
+        
+        for a in actual:
+            if not self.__match(a, expect.el_type):
+                return False
+            
+        return True
+    
     def __match_union(self, actual, union):
         for s in union.union:
             if self.__match(actual, s):
@@ -44,12 +72,15 @@ class Schema:
         return False
 
     def __match(self, val, scheme):
-        if isinstance(scheme, Schema.Any):
+        if scheme is Any:
             return True
         
         if isinstance(scheme, Schema):
             return scheme.match(val)
         
+        if isinstance(scheme, Schema.List):
+            return self.__match_list_member(val, scheme)
+        
         if isinstance(scheme, list) and isinstance(val, list):
             return self.__match_list(val, scheme)
         
index efe527e4ed913282b7928c617fc1636a87edcb9f..aafdb0ae73cc3ade0100bc6b82460620238f1d5c 100755 (executable)
@@ -6,6 +6,7 @@ from lbuild.build       import BuildEnvironment
 from lbuild.scope       import ScopeProvider
 from lcfg2.builder      import NodeBuilder
 from lcfg2.config       import ConfigEnvironment
+from lcfg2.common       import ConfigNodeError
 
 from shared.export      import ExportJsonFile
 from shared.export      import ExportHeaderFile
@@ -91,8 +92,12 @@ def main():
     opts = parser.parse_args()
     builder = LunaBuild(opts)
 
-    builder.load()
-    builder.restore()
+    try:
+        builder.load()
+        builder.restore()
+    except ConfigNodeError as e:
+        print(e)
+        exit(1)
     
     builder.visual_config()
     
index 911faddf4f6a6dfd713c82da4a46145fa31ba6c9..8c4e6706cdc72616e927583c14d70008d96b1213 100644 (file)
@@ -5,7 +5,7 @@ from .common import CmdTable, ShconfigException
 from .common import select, cmd
 
 from lcfg2.config   import ConfigEnvironment
-from lcfg2.common   import NodeProperty, NodeDependency
+from lcfg2.common   import NodeProperty, NodeDependency, ConfigNodeError
 
 class Commands(CmdTable):
     def __init__(self, env: ConfigEnvironment):
@@ -84,8 +84,14 @@ class Commands(CmdTable):
         if node is None:
             raise ShconfigException(f"no such config: {name}")
         
-        NodeProperty.Value[node] = value
-        self.__env.refresh()
+        if NodeProperty.Readonly[node]:
+            raise ShconfigException(f"node is read only")
+        
+        try:
+            NodeProperty.Value[node] = value
+            self.__env.refresh()
+        except ConfigNodeError as e:
+            print(e)
 
     @cmd("dep")
     def __fn_dep(self, name: str):