Differences

This shows you the differences between two versions of the page.

Link to this comparison view

alf:laboratoare:06 [2018/03/29 01:11]
alexandru.radovici
alf:laboratoare:06 [2021/04/09 23:33] (current)
diana.ghindaoanu
Line 1: Line 1:
-====== TP 6 - Parser du langage ​======+====== TP 6 - AST: Continuation ​======
  
-===== Parse Tree =====+L'​**AST** est une représentation arborescente de la structure syntaxique abstraite du code source écrit dans un langage de programmation. Chaque nœud de l'​arborescence désigne un jeton identifié, reconnu comme étant dans un ordre correct pour le langage défini dans la grammaire.
  
-Un parser génère un arbre, nommé parse tree. Cela reflète les règles ​de grammaire. Jison ne génère pas le parse tree par défaut et nous permet de créer cet parse tree dans n'importe quel format que nous voulons.+La structure ​de l'AST dépend beaucoup de la complexité de la grammaire: ​
  
-Un format possible est+{{ :​alf:​laboratoare:​ast.jpg?​750&​nolink }}
  
-==== Règle de la grammaire ​==== +Le but de ce TP est de vous habituer a travailler plus avec le //parser//, le //​visiteur//​ et la generation de l'//​arbre de syntaxe abstraite//,​ en continuant le dévéloppement du TP passé. 
-Le format d'un nœud d'arbre de règles de grammaire+ 
 +Etant donnée la grammaire suivante: 
 + 
 +<code g4> 
 +grammar Ex1; 
 + 
 +start               : (statement SEMICOLON NEWLINE*)* ​              #​multilineProg 
 +                    | statement SEMICOLON NEWLINE* ​                 #​singlelineProg  
 +                    ; 
 + 
 +statement ​          : declaration ​                                  #​declarationRule 
 +                    | expression ​                                   #​expressionRule 
 +                    | attribution ​                                  #​attributionRule 
 +                    ; 
 + 
 +declaration ​        : type VARIABLE EQ expression ​                  #​variableDeclaration 
 +                    ; 
 + 
 +type                : INT                                           #​typeInt 
 +                    | FLOAT                                         #​typeFloat 
 +                    | STRING ​                                       #​typeString 
 +                    ; 
 + 
 +value               : INT_NUMBER ​                                   #valueInt 
 +                    | FLOAT_NUMBER ​                                 #​valueFloat 
 +                    | STRING_TEXT ​                                  #​valueString 
 +                    | VARIABLE ​                                     #​valueVariable 
 +                    ; 
 + 
 +expression ​         : left=expression op=MUL right=expression ​      #​expressionMultiply 
 +                    | left=expression op=DIV right=expression ​      #​expressionDivision ​                   
 +                    | left=expression op=REM right=expression ​      #​expressionRem ​ 
 +                    | left=expression op=ADD right=expression ​      #​expressionAddition 
 +                    | left=expression op=SUB right=expression ​      #​expressionSubtraction 
 +                    | LP expression RP                              #​expressionParanthesis 
 +                    | value                                         #​expressionValue 
 +                    ; 
 + 
 +/** TODO 1: Add the tokens (in the Lexer part) and the rules (with aliases) for boolean expressions 
 +  * Operators: OR, AND, NOT 
 +  * Values: true, false, variables  
 + */ 
 + 
 + 
 +attribution ​        : VARIABLE EQ expression ​                       #​variableAttribution 
 +                    ; 
 + 
 + 
 +/** TODO 4: Add the tokens (in the Lexer part) and the rules (with aliases) for lists declaration 
 +  * Keyword: list 
 +  * Name: any variable name 
 +  * Values: any value separated by comma 
 + */ 
 + 
 + 
 +/** TODO 5: Add the tokens (in the Lexer part) and the rules (with aliases) for functions declaration 
 +  * Keyword: function 
 +  * Name: any variable name 
 +  * Parameters: any declaration separated by comma 
 +  * Instructions:​ any statement separated by a semicolon and one or more new lines 
 +  * Return: "​return"​ keyword + any statement ending with a semicolon  
 + */ 
 + 
 +/** BONUS: Add the tokens (in the Lexer part) and the rules (with aliases) for function calls 
 +  * Function name: any variable name 
 +  * Parameters: any value separated by comma 
 +  * Add the function call to the variable declaration ​  
 + */ 
 + 
 + 
 +WS                  :   (' ​'​) ​      -> skip; 
 +NEWLINE ​            : ​  ([\r\n]+)   -> skip; 
 +VARIABLE ​           :   ('_'​[a-zA-Z0-9]+);​ 
 +ADD                 : ​  '​+';​ 
 +SUB                 : ​  '​-';​ 
 +MUL                 : ​  '​*';​ 
 +DIV                 : ​  '/';​ 
 +REM                 : ​  '​%';​ 
 +INT                 : ​  '​int';​ 
 +FLOAT               : ​  '​float';​ 
 +STRING ​             :   '​string';​ 
 +LP                  :   '​(';​ 
 +RP                  :   '​)';​ 
 +EQ                  :   '​=';​ 
 +SEMICOLON ​          : ​  ';';​ 
 +INT_NUMBER ​         :   ​([0-9]+);​ 
 +FLOAT_NUMBER ​       :   ​([0-9]+'​.'​[0-9]+);​ 
 +STRING_TEXT ​        : ​  ​('"'​~["​]+'"'​|'​\''​~['​]+'​\''​);​
  
-<code javascript>​ 
-{ 
-  rule: '​grammar rule name', 
-  items: [] 
-} 
 </​code>​ </​code>​
  
-==== Jeton ==== +Et l'écrasement des méthodes ​de visiteur:
-Le format d'une feuille d'​arbre ​de jeton+
  
 <code javascript>​ <code javascript>​
-+import ​CharStreams,​ CodePointCharStream,​ CommonTokenStream,​ Token } from '​antlr4ts';​ 
-  ​token: ​'TOKEN', +import { Ex1Lexer } from './​Ex1Lexer.js'
-  ​value:​ value +import { Ex1ParserExpressionAdditionContext,​ ExpressionDivisionContext,​ ExpressionMultiplyContext,​ ExpressionParanthesisContext,​ ExpressionRemContext,​ ExpressionSubtractionContext,​ MultilineProgContext,​ SinglelineProgContext,​ TypeFloatContext,​ TypeIntContext,​ TypeStringContext,​ ValueFloatContext,​ ValueIntContext,​ ValueStringContext,​ VariableDeclarationContext,​ ExpressionValueContext,​ ValueVariableContext,​ VariableAttributionContext } from '​./​Ex1Parser.js';​ 
-+import { Ex1Visitor ​from '​./​Ex1Visitor.js';​ 
-</code>+import * as fs from '​fs';​ 
 +import { AbstractParseTreeVisitor } from '​antlr4ts/tree/​AbstractParseTreeVisitor';​
  
-==== Actions pour construire le parse tree ====+let input: string ​fs.readFileSync('​./​sample.txt'​).toString();​ 
 +let inputStream:​ CodePointCharStream ​CharStreams.fromString(input);​ 
 +let lexer: Ex1Lexer ​new Ex1Lexer(inputStream);​ 
 +let tokenStream:​ CommonTokenStream ​new CommonTokenStream(lexer);​ 
 +let parser: Ex1Parser ​new Ex1Parser(tokenStream);​
  
-D'​abord nous écrivons deux fonctions: une pour construire un noeud de parse tree et une autre pour construire une feuille de parse tree.+let tree = parser.start();
  
-<code javascript>​ +abstract class ASTNode { 
-function rule (rule_name, items) +    ​constructor(){};
-{ +
-  return { +
-    rule: rule_name,​ +
-    items: items +
-  ​};+
 } }
-</​code>​ 
  
-<code javascript>​ +class StatementsNode extends ASTNode { 
-function token (token_namevalue+    ​constructor(public readonly statements: ASTNode[]public readonly line: number{ 
-+        ​super();​ 
-  return { +    } 
-    ​tokentoken_name+    toJSON() ​
-    value: value +        return { 
-  };+            id: "​statements",​ 
 +            statements: this.statements,​ 
 +        } 
 +    ​
 +
 +class DeclarationNode extends ASTNode { 
 +    constructor(public readonly variable_typestringpublic readonly variable: string, public readonly op: string, public readonly value: Expression|ValueNode, ​ public readonly line: number) { 
 +        super(); 
 +    ​
 +    toJSON() { 
 +        return { 
 +            id: "​declaration",​ 
 +            variable_type:​ this.variable_type,​ 
 +            variable: this.variable,​ 
 +            ​value: ​this.value, 
 +        } 
 +    ​}
 } }
-</​code>​ 
  
-Nous allons écrire les deux fonctions dans la section %%du fichier jisondans la partie de parser.+class ValueNode extends ASTNode ​{ 
 +    constructor(public readonly value: number|string|boolean,​ public readonly line: number) { 
 +        super(); 
 +    ​} 
 +    toJSON() { 
 +        return { 
 +            id: "​value"​, 
 +            value: this.value, 
 +        } 
 +    } 
 +
 +class TypeNode extends ASTNode { 
 +    constructor(public readonly type_name: string, public readonly line: number) { 
 +        super(); 
 +    } 
 +    toJSON() { 
 +        return ​ { 
 +            type: this.type_name,​ 
 +        } 
 +    } 
 +
 +class Expression extends ASTNode { 
 +    constructor(public readonly op: string, public readonly left: Expression, public readonly right: Expression, public readonly line: number) { 
 +        super(); 
 +    } 
 +    toJSON() { 
 +        return { 
 +            id: "​expression",​ 
 +            left: this.left,​ 
 +            right: this.right,​ 
 +            op: this.op, 
 +        } 
 +    } 
 +}
  
-La règle de début renvoie le parse tree.+class AttributionNode extends ASTNode { 
 +    constructor(public readonly variable: string, public readonly value: Expression, public readonly line: number) { 
 +        super(); 
 +    } 
 +    toJSON() { 
 +        return { 
 +            id: "​attribution",​ 
 +            to: this.variable, 
 +            from: this.value,​ 
 +        } 
 +    } 
 +}
  
-<note+/** TODO 1: Create a class for List Nodes which will return a Node with the following properties 
-Si les règles de départ comportent plus d'un élémentnous devons construire le nœud dans le parse tree et le retourner. +     * id =list_declaration 
-</note>+     * list_type => the actual typewhich will be set in the Visitor as list 
 +     * list_name =the name of the list 
 +     * values => an array of values 
 +*/
  
-<code jison variable.jison>​ + 
-/* Jison example file */ +/** TODO 5: Create a class for Function Nodes which will return a Node with the following properties 
-  + * id => function 
-/Tokens part */ + ​* ​function_name:​ the actual name of the function, taken from the variable 
-%lex + parameters: the list of the parsed nodes set as parameters 
-  + * instructions:​ the list of the parsed nodes set as statements in the function 
-%% + * return: the parsed return node 
-  + */ 
-/* RegEx */ + 
-  +/** TODO 5: Recommandations for solving this exercice 
-\s+                                       /skip whitespace ​*/  + * You should create a class for the Parameter Node, with the following properties: ​ 
-var                           ​return ​'​VAR'​+        id: function_parameter,​ 
-[A-Za-z\$_][A-Za-z\$_0-9]*   ​return ​'​IDENTIFIER'​+        * type: the type of the parameter from the declaration 
-','​  ​         ​return ​','+        * value: the parsed value assigned to the parameter 
-  + * You should create a class for the Return Node, with the following properties:​ 
-/lex +        * id: return_node 
-  +        * return_statement:​ the statement that will be parsed in the Visitor  
-/* Grammar part, for this lab */ + */ 
-  + 
-%+class MyEx1Visitor extends AbstractParseTreeVisitor<​ASTNode>​ implements Ex1Visitor<​ASTNode>​ { 
-// function for grammar rule +    defaultResult() { 
-function rule (rule_nameitems+        ​return ​new StatementsNode([],​ 0)
-+    } 
-  return { +    visitMultilineProg(ctx:​ MultilineProgContext):​ StatementsNode { 
-    ​rulerule_name+        let statements = []
-    ​itemsitems +        for(let i = 0; i < ctx.statement().length;​ i++) 
-  };+            statements[i= this.visit(ctx.statement(i));​ 
 +        if(statements) { 
 +            ​return ​new StatementsNode(statements,​ 1)
 +        } else { 
 +            throw new Error(); 
 +        } 
 +    } 
 +    visitSinglelineProg(ctx:​ SinglelineProgContext):​ASTNode { 
 +        return new StatementsNode([this.visit(ctx.statement())]1); 
 +    } 
 +    visitVariableDeclaration(ctx:​ VariableDeclarationContext):​ DeclarationNode { 
 +        ​return ​new DeclarationNode( 
 +            (this.visit(ctx.type()) as TypeNode).type_name, 
 +            ctx.VARIABLE().text,​ 
 +            ctx.EQ().text,​ 
 +            this.visit(ctx.expression()) as Expression,​ 
 +            ctx.VARIABLE().symbol.line 
 +        )
 +    } 
 +    ​visitValueInt(ctx:​ ValueIntContext):​ ValueNode { 
 +        ​return new ValueNode( 
 +            ​parseInt(ctx.INT_NUMBER().text),​ 
 +            ctx.INT_NUMBER().symbol.line 
 +        ); 
 +    } 
 +    visitValueVariable(ctx:​ ValueVariableContext):​ ValueNode { 
 +        return new ValueNode( 
 +            ctx.VARIABLE().text,​ 
 +            ctx.VARIABLE().symbol.line 
 +        ); 
 +    } 
 +    visitValueFloat(ctx:​ ValueFloatContext):​ ValueNode { 
 +        return new ValueNode( 
 +            parseFloat(ctx.FLOAT_NUMBER().text),​ 
 +            ctx.FLOAT_NUMBER().symbol.line 
 +        ); 
 +    } 
 +    visitValueString(ctx:​ ValueStringContext):​ ValueNode { 
 +        return new ValueNode( 
 +            ctx.STRING_TEXT().text,​ 
 +            ctx.STRING_TEXT().symbol.line 
 +        ); 
 +    } 
 +    ​/** TODO 1: Visit the boolean value */ 
 +     
 +    ​visitTypeInt(ctx:​ TypeIntContext):​ TypeNode ​
 +        ​return new TypeNode( 
 +            ctx.INT().text,​ 
 +            ctx.INT().symbol.line 
 +        ) 
 +    } 
 +    visitTypeString(ctx:​ TypeStringContext):​ TypeNode { 
 +        return new TypeNode( 
 +            ctx.STRING().text,​ 
 +            ctx.STRING().symbol.line 
 +        ) 
 +    } 
 +    visitTypeFloat(ctx:​ TypeFloatContext):​ TypeNode { 
 +        return new TypeNode( 
 +            ctx.FLOAT().text,​ 
 +            ctx.FLOAT().symbol.line 
 +        ) 
 +    } 
 +    ​/** TODO 1: Visit the boolean type *
 + 
 +    visitExpressionMultiply(ctx: ExpressionMultiplyContext):​ Expression { 
 + const left = this.visit(ctx.expression(0));​ 
 + const right = this.visit(ctx.expression(1));​ 
 + const op = ctx._op; 
 + 
 + if(op.text) { 
 + return new Expression(op.textleft as Expression, right as Expression, ctx._op.line); 
 + } else throw new Error(); 
 +
 +    visitExpressionDivision(ctx:​ ExpressionDivisionContext):​ Expression ​
 + const left = this.visit(ctx.expression(0));​ 
 + const right = this.visit(ctx.expression(1));​ 
 + const op = ctx._op; 
 + 
 + if(op.text) { 
 + return ​new Expression(op.text,​ left as Expression, right as Expression, ctx._op.line);​ 
 + } else throw new Error(); 
 +
 +    visitExpressionRem(ctx:​ ExpressionRemContext):​ Expression ​{ 
 + const left = this.visit(ctx.expression(0));​ 
 + const right = this.visit(ctx.expression(1));​ 
 + const op = ctx._op; 
 + 
 + if(op.text) { 
 + return new Expression(op.text,​ left as Expression, right as Expression, ctx._op.line);​ 
 + } else throw new Error(); 
 + } 
 +    ​visitExpressionAddition(ctxExpressionAdditionContext):​ Expression { 
 + const left = this.visit(ctx.expression(0));​ 
 + const right = this.visit(ctx.expression(1));​ 
 + const op = ctx._op; 
 + 
 + if(op.text) { 
 + return new Expression(op.textleft as Expression, right as Expression, ctx._op.line);​ 
 + } else throw new Error(); 
 + } 
 +    ​visitExpressionSubtraction(ctxExpressionSubtractionContext):​ Expression { 
 + const left = this.visit(ctx.expression(0));​ 
 + const right = this.visit(ctx.expression(1));​ 
 + const op = ctx._op; 
 + 
 + if(op.text) { 
 + return new Expression(op.text,​ left as Expression, right as Expression, ctx._op.line);​ 
 + else throw new Error(); 
 +
 +    visitExpressionParanthesis(ctx:​ ExpressionParanthesisContext) { 
 +        return this.visit(ctx.expression());​ 
 +    } 
 +    visitExpressionValue(ctx:​ ExpressionValueContext):​ ValueNode { 
 +        let value = this.visit(ctx.value());​ 
 +        if(value !== undefined) { 
 +     return new ValueNode((this.visit(ctx.value()) as ValueNode).value,​ ctx.value()._start.line);​ 
 +        } else throw new Error(); 
 +
 +     
 +    /**TODO 1: Visit every type of boolean expression */ 
 + 
 + 
 +    /**TODO 4: Visit list declaration */ 
 +    visitVariableAttribution(ctx:​ VariableAttributionContext):​ AttributionNode { 
 +        return new AttributionNode( 
 +            ctx.VARIABLE().text,​ 
 +            this.visit(ctx.expression()) as Expression,​ 
 +            ctx.VARIABLE().symbol.line 
 +        ); 
 +    } 
 + 
 +    /**TODO 5: Visit function declaration and return node */ 
 +    ​
 } }
 +const visitor = new MyEx1Visitor();​
 +console.log(JSON.stringify(visitor.visit(tree),​ null, 4));
  
-// function for token + 
-function token (token_name,​ value)+ 
 +</code> 
 + 
 +Le résultat généré aura le format suivant: 
 + 
 +<code javascript>​
 { {
-  return { +    "​id":​ "​statements",​ 
-    ​tokentoken_name+    ​"​statements"​
-    value: value +        { 
-  };+            "​id":​ "​declaration"​
 +            "​variable_type":​ "​int",​ 
 +            "​variable":​ "​_var1",​ 
 +            "value"
 +                "​id":​ "​expression",​ 
 +                "​left":​ { 
 +                    "​id":​ "​expression",​ 
 +                    "​left":​ { 
 +                        "​id":​ "value", 
 +                        "​value":​ 2 
 +                    ​}
 +                    "​right":​ { 
 +                        "​id":​ "​value",​ 
 +                        "​value":​ 5 
 +                    }, 
 +                    "​op":​ "​+"​ 
 +                }, 
 +                "​right":​ { 
 +                    "​id":​ "​expression",​ 
 +                    "​left":​ { 
 +                        "​id":​ "​value",​ 
 +                        "​value":​ 7 
 +                    }, 
 +                    "​right":​ { 
 +                        "​id":​ "​value",​ 
 +                        "​value":​ 3 
 +                    }, 
 +                    "​op":​ "​*"​ 
 +                }, 
 +                "​op":​ "/"​ 
 +            } 
 +        } 
 +    ]
 } }
-%} 
  
-%%  +</​code>​
-  +
-start: variable  +
-+
- return $1; +
- }; +
- +
  
-variable:​ VAR variables +===== Erreurs ===== 
-+Au niveau de l'anayse on peut retrouver 2 types d'erreurs: 
- $$ ​rule ('​variable',​ [token ('​VAR',​ $1), $2]); +   * **Erreurs lexicales** - le Parser trouve des jetons qu'il ne peut pas identifier dans le Lexer 
- }; +   * **Erreurs syntaxiques** - le texte ne respecte pas les règles indiquées dans la grammaire
- +
-variables:​ IDENTIFIER ','​ variables ​   +
-+
- $$ ​rule ('​variables', ​ +
-            [ +
-               token ('IDENTIFIER', $1),  +
-               token (',', ','​), ​ +
-               $3 +
-            ] +
-           ); +
-+
- | IDENTIFIER  +
-+
- $$ = token ('​IDENTIFIER',​ $1); +
- };+
  
  
 +Pour pouvoir traiter ces erreurs, on doit tout d'​abord éliminer les écouteurs d'​erreurs par défaut d'​ANTLR (ErrorListeners)
 +<code javascript>​
 +lexer.removeErrorListeners ();
 +parser.removeErrorListeners ();
 </​code>​ </​code>​
  
 +Ensuite, il faut créer un nouvel outil qui puisse attraper et afficher les erreurs. Pour cela, on peut définir une nouvelle classe:
  
-===== Exercises =====+<code javascript>​ 
 +class ErrorPrinter implements ANTLRErrorListener<​any>​ { 
 +    syntaxError (recognizer:​ Recognizer<​any,​ any>, offendingSymbol:​ any | undefined, line: number, charPositionInLine:​ number, msg: string, e: RecognitionException | undefined) { 
 +        if (e) {    
 +            console.log ("​line:​ "​+line+"​ positon: "​+charPositionInLine+"​ message: "​+msg);​
  
-Pour les exercises accessez [[https://​nolanlawson.github.io/​jison-debugger/​|Jison Debugger]].+        } 
 +    }  
 +
 +</code>
  
-  - Ajoutez les règles de grammaire pour les expressions (plusieurs expressions) du laboratoire précédent à la grammaire décrite dans l'exemple. Un programme possède des déclarations ​et expressions variables. Le delimitateur des instructions est ';'. (**3p**) +Finalament, on doit indiquer au ''​parser'​' et au ''​lexer''​ quels sont les nouveaux écouteurs:
-  - Ajouter une attribution de valeur variable. La règle pour cela est <​code>​variable = value</​code>​. La valeur est un nombre. Pour utiliser une variable dans une expression, elle doit être déjà définie. (**2p**) +
-  - Ajouter des règles pour que les variables puissent prendre des valeurs qui sont des expressions. (**1p**) +
-  - Ajouter des appels de fonction à la grammaire. Un appel de fonction ressemble à <​code>​function_name (parameter1,​ parameter2, parameter3, ...)</​code>​. (**4p**)+
  
-<hidden+<code javascript
-==== Solutions ==== +lexer.addErrorListener (new ErrorPrinter());​ 
-[[https://​github.com/​alexandruradovici/​alf/​tree/​master/​TP6|Solutions]] +parser.addErrorListener (new ErrorPrinter());​ 
-</hidden>+</code>
  
 +===== Exercices ======
 +
 +   - Téléchargez la [[https://​github.com/​UPB-FILS-ALF/​TP/​tree/​main/​TP6|structure du TP]] et utilisez la grammaire **Ex1.g4** comme support pour les exercices. En suivant les lignes marquées par TODO 1, ajoutez les règles de grammaire, les classes et les méthodes nécessaires pour pouvoir déclarer aussi des variables et des expressions booléennes. Testez la fonctionnalité du programme pour l'​exemple suivant: **(2p)** <​code>​ bool _var1 = _var2 ||_var3 && !_var4; </​code>​
 +   - Apportez les modifications nécessaires de sorte que chaque noeud de l'AST comprend aussi le numéro de la ligne ou il a été trouvé. **(1.5p)**
 +   - Faites que la grammaire accepte aussi la concaténation des chaines de caracteres. Vérifiez pour l'​exemple suivant: **(1.5p)** <​code>​ string _var2 = "FILS " + "​ALF";​ </​code>​
 +   - En suivant les lignes marquées par TODO 4, ajoutez à la grammaire les règles, classes et méthodes nécessaires pour que vous puissiez déclarer des listes. Les listes peuvent inclure n'​importe quel type de données . Vérifiez pour la déclaration suivante: **(1.5p)** <​code>​ list _var1 = [10, 5.5, '​alf',​ true, _var2]; </​code>​
 +   - En suivant les lignes marquées par TODO 5, ajoutez les règles, les classes et les méthodes necessaires pour la déclaration des fonctions. Une fonction peut avoir un ou plusieurs paramètres et son contenu peut inclure une seule ou plusieurs instructions. Vérifiez la correctitude de votre programme pour l'​exemple suivant: **(3.5p)** <​code>​function _functionName (int _var1=0, string _var2="​alf",​ bool _var3=true)
 +{
 +    _var1 = _var2 + "​2021";​
 +    _var4 = false;
  
 +    return 3+5/7;
 +};</​code>​
 +   - **BONUS**: Traitez les erreurs de lexer et de syntaxe pour votre arbre et affichez les messages nécessaires. **(1.5p)** ​
alf/laboratoare/06.1522275109.txt.gz · Last modified: 2018/03/29 01:11 by alexandru.radovici
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0