Johan Rong пре 2 дана
родитељ
комит
230fd361a2
5 измењених фајлова са 169 додато и 18 уклоњено
  1. 8 0
      README.md
  2. 9 14
      examples/testing/src/main.zk
  3. 3 0
      src/zelkelbt/build.py
  4. 23 2
      src/zelkelc/ast/parser.py
  5. 126 2
      src/zelkelc/expander/expander.py

+ 8 - 0
README.md

@@ -18,6 +18,14 @@ Lexer --> Preprocessor (unimplemented) --> Parser --> Expander --> Validator -->
   - explicit value with * or explicit pointer with &. You can also use & and * on a "dynamic" type to get both, "dynamic" types are both at the same time, but in reality they are probably the pointer and then you know both 
 
 ## Planning code + ideas + sketch, ... etc ([see also](https://github.com/zelkel-lang/zelkel-syntax-demo))
+### Macros
+```rust
+#fn name($x: expr, $y: ident) {
+    ... template tokens ...
+}
+```
+
+### Just a bunch of stuff
 ```kotlin
 class Parser {
   val tokens: Arr<Token>

+ 9 - 14
examples/testing/src/main.zk

@@ -1,23 +1,18 @@
 #llvm(
     #"@int.fmt = private unnamed_addr constant [14 x i8] c"Integer: %ld\0A\00""#
 )
-#llvm(
-    #"@float.fmt = private unnamed_addr constant [12 x i8] c"Float: %lf\0A\00""#
-)
-
 #llvm("declare i32 @printf(ptr, ...)")
 
-fn main() -> void {
-    val test: i64 = 123
-    val test2: f64 = 12.3 + 5.6 * 3.2
-
+#fn print_i64($x: expr) {
     #llvm(
         "%call.{:id} = call i32 (ptr, ...) @printf(ptr @int.fmt, {0:ty} {0})",
-        test
-    )
-    #llvm(
-        "%call.{:id} = call i32 (ptr, ...) @printf(ptr @float.fmt, {0:ty} {0})",
-        test2
+        $x
     )
+}
+
+fn main() -> void {
+    val test: i64 = 123 * 2
+    #print_i64(test)
+    #print_i64(test + 1)
     return
-}
+}

+ 3 - 0
src/zelkelbt/build.py

@@ -50,6 +50,9 @@ def build_package(build_mode: str, config: dict[str, any], debug_info: bool):
         rich.print(ast)
 
     ast = Expander(ids).run(ast)
+    if debug_info:
+        rich.print(ast)
+    
     Validator(ast)
 
     codegen = Codegen(ast)

+ 23 - 2
src/zelkelc/ast/parser.py

@@ -122,10 +122,31 @@ class Parser:
         self.expect(token.Pound())
         ident = self.expect_type(token.Identifier)
         if ident.value == "fn":
-            # return self.parse_macro_definition()
-            raise ParseError("Implemented parse_macro_definition")
+            return self.parse_macro_definition()
         return self.parse_macro_call(ident)
 
+
+    def parse_macro_definition(self) -> types.MacroDefinition:
+        name = self.expect_type(token.Identifier).value
+
+        self.expect(token.LParen())
+        params: list[tuple[str, str]] = []
+        while not self.current(token.RParen()):
+            if params:
+                self.expect(token.Comma())
+            self.expect(token.Dollar())
+            pname = self.expect_type(token.Identifier).value
+            self.expect(token.Colon())
+            kind = self.expect_type(token.Identifier).value
+            if kind not in ("expr", "ident", "type", "tt"):
+                raise ParseError(f"Unknown macro fragment kind: {kind}")
+            params.append((pname, kind))
+        self.expect(token.RParen())
+
+        # Body is a { ... } token tree, stored unparsed
+        body = self.take_delimited()
+        return types.MacroDefinition(name=name, params=params, body=body)
+
     def parse_macro_call(self, ident: token.Identifier) -> types.MacroCall:
         args = self.take_delimited()
         return types.MacroCall(name=ident.value, args=args)

+ 126 - 2
src/zelkelc/expander/expander.py

@@ -1,3 +1,4 @@
+import copy
 import re
 from itertools import count
 
@@ -38,9 +39,18 @@ class Expander:
     def __init__(self, ids: count):
         self.ids = ids
         self.expansion_id = 0
+        self.macros: dict[str, types.MacroDefinition] = {}
 
     def run(self, nodes: list[types.Node]) -> list[types.Node]:
-        return self.expand_nodes(nodes, depth=0)
+        remaining: list[types.Node] = []
+        for n in nodes:
+            if isinstance(n, types.MacroDefinition):
+                if n.name in self.macros:
+                    raise MacroError(f"macro #{n.name} already defined")
+                self.macros[n.name] = n
+            else:
+                remaining.append(n)
+        return self.expand_nodes(remaining, depth=0)
 
     def expand_nodes(self, nodes: list[types.Node], depth: int) -> list[types.Node]:
         if depth > 64:
@@ -62,9 +72,22 @@ class Expander:
         return out
 
     def expand_call(self, call: types.MacroCall) -> list[types.Node]:
+        # Builtins
         if call.name == "llvm":
             return [self.expand_llvm(call)]
-        raise MacroError(f"unknown macro #{call.name}")
+
+        macro = self.macros.get(call.name)
+        if macro is None:
+            raise MacroError(f"unknown macro #{call.name}")
+
+        bindings = self.match(macro, call.args)
+        transcribed = self.transcribe(macro.body, bindings)
+
+        sub = Parser(transcribed, 0, self.ids)
+        try:
+            return sub.parse_statements("_macro_", stop_at_rbrace=False)
+        except ParseError as e:
+            raise MacroError(f"in expansion of #{call.name}: {e}") from e
 
     def expand_llvm(self, call: types.MacroCall) -> types.InlineIR:
         tokens = call.args
@@ -93,3 +116,104 @@ class Expander:
         eid = self.expansion_id
         self.expansion_id += 1
         return types.InlineIR(template=template, args=arg_exprs, expansion_id=eid)
+
+    def match(
+        self, macro: types.MacroDefinition, tokens: list[token.Token]
+    ) -> dict[str, list[token.Token]]:
+        bindings: dict[str, list[token.Token]] = {}
+        i = 0
+        for name, kind in macro.params:
+            if i > 0:
+                if i >= len(tokens) or not isinstance(tokens[i], token.Comma):
+                    raise MacroError(f"expected ',' between arguments to #{macro.name}")
+                i += 1
+
+            sub = Parser(tokens, i, self.ids)
+            try:
+                match kind:
+                    case "expr":
+                        expr = sub.parse_expression()
+                        if expr is None:
+                            raise ParseError("expected expression")
+                        bindings[name] = [token.Interpolated(expr)]
+
+                    case "ident":
+                        ident = sub.expect_type(token.Identifier)
+                        bindings[name] = [ident]
+
+                    case "type":
+                        typ = sub.parse_type(False)
+                        type_name = {
+                            types.Integer64: "i64",
+                            types.Float64: "f64",
+                            types.String: "str",
+                            types.Void: "void",
+                        }.get(typ, "unknown")
+                        bindings[name] = [token.Identifier(type_name)]
+
+                    case "tt":
+                        start = sub.cursor
+                        depth = 0
+                        while sub.cursor < len(tokens):
+                            t = tokens[sub.cursor]
+                            if isinstance(t, token.Comma) and depth == 0:
+                                break
+                            if isinstance(t, (token.LParen, token.LBrace)):
+                                depth += 1
+                            elif isinstance(t, (token.RParen, token.RBrace)):
+                                depth -= 1
+                            sub.cursor += 1
+                        bindings[name] = tokens[start : sub.cursor]
+
+                    case _:
+                        raise MacroError(f"unknown fragment kind {kind}")
+            except ParseError as e:
+                raise MacroError(
+                    f"failed to match '{name}:{kind}' of #{macro.name}: {e}"
+                ) from e
+            i = sub.cursor
+
+        if i != len(tokens):
+            raise MacroError(f"unexpected tokens in call to #{macro.name}")
+        return bindings
+
+    def transcribe(
+        self, template: list[token.Token], bindings: dict[str, list[token.Token]]
+    ) -> list[token.Token]:
+        hygiene_map: dict[str, str] = {}
+        eid = next(self.ids)
+        j = 0
+        while j < len(template):
+            t = template[j]
+            if (
+                isinstance(t, token.Identifier)
+                and t.value in ("val", "var")
+                and j + 1 < len(template)
+                and isinstance(template[j + 1], token.Identifier)
+            ):
+                old = template[j + 1].value
+                if old not in hygiene_map:
+                    hygiene_map[old] = f"{old}__{eid}"
+            j += 1
+
+        out: list[token.Token] = []
+        i = 0
+        while i < len(template):
+            t = template[i]
+            if isinstance(t, token.Dollar) and i + 1 < len(template):
+                nxt = template[i + 1]
+                if isinstance(nxt, token.Identifier) and nxt.value in bindings:
+                    for b in bindings[nxt.value]:
+                        if isinstance(b, token.Interpolated):
+                            out.append(token.Interpolated(copy.deepcopy(b.node)))
+                        else:
+                            out.append(b)
+                    i += 2
+                    continue
+
+            if isinstance(t, token.Identifier) and t.value in hygiene_map:
+                out.append(token.Identifier(hygiene_map[t.value]))
+            else:
+                out.append(t)
+            i += 1
+        return out