From 2843300cf9d8ef2e4c08c26813ec44985958be23 Mon Sep 17 00:00:00 2001 From: KenanZhu <3471685733@qq.com> Date: Sun, 17 May 2026 01:48:25 +0800 Subject: [PATCH] =?UTF-8?q?refactor(autoscript):=20=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E8=A7=82=E5=AF=9F=E8=80=85=E6=A8=A1=E5=BC=8F=E8=A7=A3=E8=80=A6?= =?UTF-8?q?=E8=A7=A3=E6=9E=90=E4=B8=8E=E9=A2=84=E6=A3=80=E6=9F=A5/?= =?UTF-8?q?=E7=BC=96=E6=8E=92=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoscript/ASObserver.py | 78 ++++++++++++++++++++ src/autoscript/ASTokenizer.py | 135 +++++++++++++++++++++++++++------- src/autoscript/__init__.py | 17 +---- 3 files changed, 191 insertions(+), 39 deletions(-) create mode 100644 src/autoscript/ASObserver.py diff --git a/src/autoscript/ASObserver.py b/src/autoscript/ASObserver.py new file mode 100644 index 0000000..7fa3138 --- /dev/null +++ b/src/autoscript/ASObserver.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +""" +Copyright (c) 2026 KenanZhu. +All rights reserved. + +This software is provided "as is", without any warranty of any kind. +You may use, modify, and distribute this file under the terms of the MIT License. +See the LICENSE file for details. +""" + + +class ParsingObserver: + """ + Base observer for AutoScript parsing events. + + Subclass and override the relevant methods to react to + tokenization / parsing events produced by ASTokenizer. + This is the core abstraction that lets pre-check and + orchestration modules subscribe to the same parsing pipeline. + """ + + def onParseStart( + self, + script_text: str + ): + """ + Called when tokenization of a new script begins. + + Args: + script_text (str): The full script text being parsed. + """ + pass + + def onTokenParsed( + self, + kind: str | None, + data, + line_num: int, + raw_line: str + ): + """ + Called after each script line has been classified as a token. + + Args: + kind (str | None): Token kind (K_IF, K_ELSE_IF, K_ELSE, + K_ENDIF, K_SET, K_ADD, K_SUB) or + None if unrecognised. + data: Token payload — condition string for IF/ELSE IF, + (target, value) tuple for SET/ADD/SUB, + None for ELSE/ENDIF/unrecognised. + line_num (int): 1-based line number. + raw_line (str): The stripped raw line text. + """ + pass + + def onParseComplete( + self, + statements: list + ): + """ + Called when flat tokenization is complete. + + Args: + statements (list[Stmt]): The list of parsed Stmt objects. + """ + pass + + def onASTReady( + self, + ast + ): + """ + Called when full AST construction is complete (after parse()). + + Args: + ast (Script): The root Script AST node. + """ + pass diff --git a/src/autoscript/ASTokenizer.py b/src/autoscript/ASTokenizer.py index aa0723e..bdd2809 100644 --- a/src/autoscript/ASTokenizer.py +++ b/src/autoscript/ASTokenizer.py @@ -326,12 +326,27 @@ class ASTokenizer: """ Tokenizer / parser for the AutoScript DSL. - Provides three entry points: + Main class-level entry points (engine-facing): - classifyLine(line) — single-line classifier. - tokenize(script) — flat Stmt list. - parse(script) — structured AST (Script root). + + Observer-enabled API (used by pre-check & orchestration): + >>> obs = ScriptPrecheckObserver() + >>> stmts = ASTokenizer.tokenizeWithObservers(script, [obs]) """ + @classmethod + def _notifyObservers( + cls, + observers: list, + method: str, + *args + ): + + for obs in observers: + getattr(obs, method)(*args) + @classmethod def _matchLine( cls, @@ -345,18 +360,29 @@ class ASTokenizer: return (None, None) @classmethod - def classifyLine( + def _buildStmt( cls, - stripped: str - ): + stripped: str, + kind: str | None, + data + ) -> Stmt: - kind, data = cls._matchLine(stripped) - if kind is None or kind == K_PASS: - return None - return (kind, data) + stmt = Stmt(kind=kind, raw_line=stripped) + if kind == K_IF or kind == K_ELSE_IF: + stmt.condition = data + elif kind == K_SET: + stmt.target, stmt.value = data + stmt.op_type = OP_SET + elif kind == K_ADD: + stmt.target, stmt.value = data + stmt.op_type = OP_ADD + elif kind == K_SUB: + stmt.target, stmt.value = data + stmt.op_type = OP_SUB + return stmt @classmethod - def tokenize( + def _tokenizeImpl( cls, script: str ) -> list: @@ -367,29 +393,15 @@ class ASTokenizer: if not stripped: continue kind, data = cls._matchLine(stripped) - stmt = Stmt(kind=kind, raw_line=stripped) - - if kind == K_IF or kind == K_ELSE_IF: - stmt.condition = data - elif kind == K_SET: - stmt.target, stmt.value = data - stmt.op_type = OP_SET - elif kind == K_ADD: - stmt.target, stmt.value = data - stmt.op_type = OP_ADD - elif kind == K_SUB: - stmt.target, stmt.value = data - stmt.op_type = OP_SUB - statements.append(stmt) + statements.append(cls._buildStmt(stripped, kind, data)) return statements @classmethod - def parse( + def _parseTokens( cls, - script: str + tokens: list ) -> Script: - tokens = cls.tokenize(script) body = [] i = 0 while i < len(tokens): @@ -420,6 +432,77 @@ class ASTokenizer: i += 1 return Script(body=body) + @classmethod + def classifyLine( + cls, + stripped: str + ): + + kind, data = cls._matchLine(stripped) + if kind is None or kind == K_PASS: + return None + return (kind, data) + + @classmethod + def tokenize( + cls, + script: str + ) -> list: + + return cls._tokenizeImpl(script) + + @classmethod + def parse( + cls, + script: str + ) -> Script: + + return cls._parseTokens(cls._tokenizeImpl(script)) + + @classmethod + def tokenizeWithObservers( + cls, + script: str, + observers: list + ) -> list: + """ + Tokenize and notify observers for each classified line. + + Fires onParseStart, onTokenParsed, and onParseComplete + events to each observer. This is the single tokenization + pipeline shared by pre-check and orchestration modules. + """ + + cls._notifyObservers(observers, "onParseStart", script) + statements = [] + for i, raw_line in enumerate(script.split("\n"), 1): + stripped = raw_line.strip() + if not stripped: + continue + kind, data = cls._matchLine(stripped) + cls._notifyObservers(observers, "onTokenParsed", kind, data, i, stripped) + statements.append(cls._buildStmt(stripped, kind, data)) + cls._notifyObservers(observers, "onParseComplete", statements) + return statements + + @classmethod + def parseWithObservers( + cls, + script: str, + observers: list + ) -> Script: + """ + Parse and notify observers throughout the pipeline. + + Calls tokenizeWithObservers (which fires per-token events), + then builds the AST and fires onASTReady. + """ + + tokens = cls.tokenizeWithObservers(script, observers) + ast = cls._parseTokens(tokens) + cls._notifyObservers(observers, "onASTReady", ast) + return ast + @classmethod def _parseIfBlock( cls, diff --git a/src/autoscript/__init__.py b/src/autoscript/__init__.py index 6c6ae75..c761e6d 100644 --- a/src/autoscript/__init__.py +++ b/src/autoscript/__init__.py @@ -1,17 +1,6 @@ """ AutoScript module for the AutoLibrary project. - - A lightweight scripting DSL for preprocessing user reservation data - in repeatable timer tasks. Supports IF/ELSE IF/ELSE/END IF control - flow, SET assignments, .ADD./.SUB. operations, and rich comparisons. - - Public API: - - execute(script_text, target_data): Execute an AutoScript. - - addTargetVar(name, var_type, key_path, display_name): Register a variable. - - registerDefaultTargetVars(): Register all built-in target variables. - - META_VARS: dict of built-in read-only meta variables. - - ALL_VARIABLES: dict of all available variables (display_name -> (name, type)). - - ASTokenizer: Unified tokenizer for the orchestration dialog and engine. + A lightweight scripting DSL for preprocessing user reservation data. """ from autoscript.ASTokenizer import ( ASTokenizer, @@ -27,6 +16,7 @@ from autoscript.ASEngine import ( addTargetVar, ) from autoscript.ASObject import _META_VARS as META_VARS +from autoscript.ASObserver import ParsingObserver __all__ = [ "execute", @@ -41,7 +31,8 @@ __all__ = [ "IfNode", "SetNode", "OpNode", - "ElifNode" + "ElifNode", + "ParsingObserver", ] # All variables available to scripts (display_name -> (name, type)).