From 289e516fe3d91b935e74c29e698d0d06815efd0d Mon Sep 17 00:00:00 2001 From: Nat Quayle Nelson Date: Tue, 25 Apr 2023 11:38:12 -0600 Subject: [PATCH] Squashed 'projects/tink_syntaxhub/' content from commit 99dbb477 git-subtree-dir: projects/tink_syntaxhub git-subtree-split: 99dbb4774745300a01367e9f371f917208163477 --- .gitignore | 1 + .haxerc | 4 + .travis.yml | 51 +++++++ LICENSE | 22 +++ README.md | 189 ++++++++++++++++++++++++++ extraParams.hxml | 1 + haxe_libraries/hx3compat.hxml | 3 + haxe_libraries/hxnodejs.hxml | 7 + haxe_libraries/tink_chunk.hxml | 3 + haxe_libraries/tink_cli.hxml | 8 ++ haxe_libraries/tink_core.hxml | 3 + haxe_libraries/tink_io.hxml | 5 + haxe_libraries/tink_macro.hxml | 4 + haxe_libraries/tink_priority.hxml | 3 + haxe_libraries/tink_streams.hxml | 6 + haxe_libraries/tink_stringly.hxml | 4 + haxe_libraries/tink_syntaxhub.hxml | 4 + haxe_libraries/travix.hxml | 6 + haxelib.json | 24 ++++ src/tink/SyntaxHub.hx | 106 +++++++++++++++ src/tink/syntaxhub/ExprLevelSyntax.hx | 58 ++++++++ src/tink/syntaxhub/FrontendContext.hx | 160 ++++++++++++++++++++++ src/tink/syntaxhub/FrontendPlugin.hx | 6 + tests.hxml | 10 ++ tests/HelloWorld.txt | 1 + tests/HelloWorld.xml | 2 + tests/Main.hx | 19 +++ tests/TxtFrontend.hx | 27 ++++ tests/XmlFrontend.hx | 33 +++++ tink_syntaxhub.hxml | 9 ++ 30 files changed, 779 insertions(+) create mode 100644 .gitignore create mode 100644 .haxerc create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 extraParams.hxml create mode 100644 haxe_libraries/hx3compat.hxml create mode 100644 haxe_libraries/hxnodejs.hxml create mode 100644 haxe_libraries/tink_chunk.hxml create mode 100644 haxe_libraries/tink_cli.hxml create mode 100644 haxe_libraries/tink_core.hxml create mode 100644 haxe_libraries/tink_io.hxml create mode 100644 haxe_libraries/tink_macro.hxml create mode 100644 haxe_libraries/tink_priority.hxml create mode 100644 haxe_libraries/tink_streams.hxml create mode 100644 haxe_libraries/tink_stringly.hxml create mode 100644 haxe_libraries/tink_syntaxhub.hxml create mode 100644 haxe_libraries/travix.hxml create mode 100644 haxelib.json create mode 100644 src/tink/SyntaxHub.hx create mode 100644 src/tink/syntaxhub/ExprLevelSyntax.hx create mode 100644 src/tink/syntaxhub/FrontendContext.hx create mode 100644 src/tink/syntaxhub/FrontendPlugin.hx create mode 100644 tests.hxml create mode 100644 tests/HelloWorld.txt create mode 100644 tests/HelloWorld.xml create mode 100644 tests/Main.hx create mode 100644 tests/TxtFrontend.hx create mode 100644 tests/XmlFrontend.hx create mode 100644 tink_syntaxhub.hxml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..c5e82d74 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin \ No newline at end of file diff --git a/.haxerc b/.haxerc new file mode 100644 index 00000000..71405466 --- /dev/null +++ b/.haxerc @@ -0,0 +1,4 @@ +{ + "version": "4.2.5", + "resolveLibs": "scoped" +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..ccce6456 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,51 @@ +sudo: required +dist: trusty + +stages: + - test + - deploy + +language: node_js +node_js: 8 + +cache: + directories: + - $HOME/haxe + +os: + - linux + # - osx + +env: + - HAXE_VERSION=3.4.7 + - HAXE_VERSION=latest + + +before_install: + - args=() + - if [ "$HAXE_VERSION" == "latest" ]; then args+=(-lib); args+=(hx3compat); fi + +install: + - npm i -g lix + - lix install haxe $HAXE_VERSION + - lix download + +script: + - lix run travix interp "${args[@]}" + + +jobs: + include: + # - stage: test # should uncomment this when there is no matrix above (e.g. only one os, one env, etc) + - stage: deploy + os: linux + install: + - npm i -g lix + - lix download + script: skip + env: + - secure: "j3U6/DIvvuRRSls2Y4MOff7eEqVwjt9H6qdvvuPMvBshYB40mlAmkPzGxaVBZ1ia1DOqPmlCsHX3FwrQl+n4lAWPiyWzKaThK8Tb5TkOF1OJxHbbtzVz40ZTKBdOyowwEewxnFUO1eUs4NuOYHYTiZBPKA024oebm/1z7HJkx3k=" + + after_success: + - lix run travix install + - lix run travix release diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1c48dc07 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 haxetink + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 00000000..6c4b62c3 --- /dev/null +++ b/README.md @@ -0,0 +1,189 @@ +# Tinkerbell Syntax Hub +[![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?maxAge=2592000)](https://gitter.im/haxetink/public) + +As you add more and more macros to a code base, they begin stepping onto each others feet. The issue in fact arose a lot in the development of `tink_lang` which for a long time had its own plugin in system to make it perform its magic in an orderly fashion. This plugin system has been extracted and expanded to `tink_syntaxhub` which provides a plugin architecture for 4 things: + +1. A plugin point for additional macro based frontends +2. A plugin point for expression level syntax sugar +3. A plugin point for class wide build macros +4. A plugin point for macros that need to modify just the main function + +With the advent of `haxe.macro.Compiler.addGlobalMetadata` it is possible to define global build macros and that is what `tink_syntaxhub` does: register one global build macro that runs all of the plugins in an orderly fashion. + +## Basic structure + +The syntax hub is organized on `tink_priority` queues, which in allow for plugins to take priority over one another. This still means that if two libraries conflict, one of them must resolve the conflict by registering its steps so they no longer conflict with those of the other library (by either running sooner or later or whatever). While not perfect, it is a step forward from having to make changes for both libraries, possibly introducing more dependencies. Being based on `tink_priority`, a dependency is only loosely expressed against IDs, which are just arbitrary strings, although they should reflect fully qualified class names - they in fact do this by default. + +## Additional macro based frontends + +By using `haxe.macro.Context.onTypeNotFound`, you can add additional frontends to the haxe compiler. With `tink_syntaxhub` this should turn out a little less raw. A frontend is expressed like so: + +```haxe +interface FrontendPlugin { + function extensions():Iterator; + function parse(file:String, context:FrontendContext):Void; +} +``` + +There's not much to it. Before we go into detail and look at what a FrontendContext is, let's have an example. + +### Example Frontend + +Let's build our own silly frontend! One that takes text files and turns them into classes with one static property. + +```haxe +import tink.syntaxhub.*; +import haxe.macro.Expr; +import haxe.macro.Context; + +class TxtFrontend implements FrontendPlugin { + + public function new() {} + + public function extensions() + return ['txt'].iterator(); + + public function parse(file:String, context:FrontendContext):Void { + + var text = sys.io.File.getContent(file); + var pos = Context.makePosition({ file: file, min: 0, max: text.length }); + + context.getType().fields.push({ + name: 'TEXT', + access: [AStatic, APublic], + kind: FProp('default', 'null', macro : String, macro $v{text}), + pos: pos, + }); + } + static function use() + tink.SyntaxHub.frontends.whenever(new TxtFrontend()); +} +``` + +Put a `HelloWorld.txt` in your classpath and compile this with `haxe --macro TxtFrontend.use() -main Main --interp` : + +```haxe +class Main { + static function main() + trace(HelloWorld.TEXT); +} +``` + +Et voila! Awesome sauce! So hey, why not do the same for XMLs? + +```haxe +import tink.syntaxhub.*; +import haxe.macro.Expr; +import haxe.macro.Context; + +class XmlFrontend implements FrontendPlugin { + + public function new() {} + + public function extensions() + return ['xml'].iterator(); + + public function parse(file:String, context:FrontendContext):Void { + + var text = sys.io.File.getContent(file); + var pos = Context.makePosition({ file: file, min: 0, max: text.length }); + + try + Xml.parse(text) + catch (e:Dynamic) + Context.error('Failed to parse $file because: $e', pos); + + context.getType().fields.push({ + name: 'XML', + access: [AStatic, APublic], + kind: FProp('default', 'null', macro : Xml, macro Xml.parse($v{text})), + pos: pos, + }); + } + static function use() + tink.SyntaxHub.frontends.whenever(new XmlFrontend()); +} +``` + +Add a `HelloWorld.xml` in your classpath and this time compile with `haxe --macro TxtFrontend.use() --macro XmlFrontend.use() -main Main --interp`: + +```haxe +class Main { + static function main() { + trace(HelloWorld.TEXT); + trace(HelloWorld.XML); + } +} +``` + +So now both frontends affect the same class. That was easy, right? You can use the tests to see a working setup. + +### The Frontend API + +Let's recall what a frontend is: + +```haxe +interface FrontendPlugin { + function extensions():Iterator; + function parse(file:String, context:FrontendContext):Void; +} +``` + +When the compiler cannot find a specific file, the syntax hub looks through all classpaths looking for files that have extensions matching any of the registered frontends and then leaves the parsing to said frontends. In the above example, we asked for `HelloWorld`, for which no `.hx` file exists. The two frontends jumped in and declared the class and each added a static field to it. + +Now to understand *how* a frontend would do its work, we need to know what `FrontendContext` is. A context represents an interface to building the module that was not found by the Haxe compiler. This is what it looks like: + +```haxe +class FrontendContext { + + public var name(default, null):String; + public var pack(default, null):Array; + + public function getType(?name:String, ?orCreate:tink.core.Lazy):TypeDefinition; + + public function addDependency(file:String):Void; + public function addImport(name:String, mode:ImportMode, pos:Position):Void; + public function addUsing(name:String, pos:Position):Void; +} +``` + +First we have the name and the package of the module beeing processed. The last three calls are also quite self explanatory, assuming you are familiar with `haxe.macro.Context`. The little magic there is, is in `getType`, which if no name is supplied gets the module's main type. If the requested type was not yet created, you get to create one with the `orCreate` argument. It defaults to `macro class {}` but you may find more complex use cases. + +### Registering Frontends + +You register a `FrontendPlugin` on the `tink.SyntaxHub.frontends` priority queue. No magic here. + +### Implement frontend as class level macro + +The suggested way of implementing a frontend is to actually by pushing down the heavy lifting to a class level macro. So instead of constructing the whole class in your `FrontendPlugin` it is wiser to generate an empty class with a `@:build` directive that then fills the class. This approach leads to more understandable error messages and also helps to reduce loops. + +## Expression level syntax sugar + +Under `tink.SyntaxHub.exprLevel` you will find an object defined like this: + +```haxe +class ExprLevelSyntax { + public var inward(default, null):Queue; + public var outward(default, null):Queue; + public var id(default, null):ID; +} + +typedef ExprLevelRule = { + function appliesTo(c:ClassBuilder):Bool; + function apply(e:Expr):Expr; +} +``` + +First, let's examine what an `ExprLevelRule` is. That's where you plugin in your magic. The `appliesTo` method should tell `tink_syntaxhub` whether the rule should be applied to the current class, and if so, `apply` is given practically every expression found in that class. For example all `tink_lang` syntax rules implement their `appliesTo` function with `c.target.meta.has(':tink')`. Your implementation of `appliesTo` should not cause side effects if possible. + +Now, what's the `inward` and `outward` stuff all about? When the rules are applied, complex expressions are first traversed inward, i.e. from the outside to the inside or from the root to the leafs if you will, and then back outward. This nuance becomes particularly interesting when certain syntaxes are being nested into one another. + +## Class level syntax sugar + +You will find `tink.SyntaxHub.classLevel` to define a `QueueBool>`. All registered plugins are called in order of priority and if none of them returns `true`, then the class will be considered unmodified and the build macro will thus return `null`. + +In this queue, there is already one item under the the same ID as `tink.SyntaxHub.exprLevel.id`. Use that to either run before or after expression level plugins. + +## Modifying the main function + +This is no doubt the least spectacular bit. You will find `tink.SyntaxHub.mainTransform` to define a `QueueExpr>`, which passes the main functions body to each plugin in order of priority. Nothing fancy, but very handy! diff --git a/extraParams.hxml b/extraParams.hxml new file mode 100644 index 00000000..19df9049 --- /dev/null +++ b/extraParams.hxml @@ -0,0 +1 @@ +--macro tink.SyntaxHub.use() \ No newline at end of file diff --git a/haxe_libraries/hx3compat.hxml b/haxe_libraries/hx3compat.hxml new file mode 100644 index 00000000..77e7377e --- /dev/null +++ b/haxe_libraries/hx3compat.hxml @@ -0,0 +1,3 @@ +# @install: lix --silent download "haxelib:/hx3compat#1.0.4" into hx3compat/1.0.4/haxelib +-cp ${HAXE_LIBCACHE}/hx3compat/1.0.4/haxelib/std +-D hx3compat=1.0.4 \ No newline at end of file diff --git a/haxe_libraries/hxnodejs.hxml b/haxe_libraries/hxnodejs.hxml new file mode 100644 index 00000000..577e96a5 --- /dev/null +++ b/haxe_libraries/hxnodejs.hxml @@ -0,0 +1,7 @@ +# @install: lix --silent download "haxelib:/hxnodejs#12.1.0" into hxnodejs/12.1.0/haxelib +-cp ${HAXE_LIBCACHE}/hxnodejs/12.1.0/haxelib/src +-D hxnodejs=12.1.0 +--macro allowPackage('sys') +# should behave like other target defines and not be defined in macro context +--macro define('nodejs') +--macro _internal.SuppressDeprecated.run() diff --git a/haxe_libraries/tink_chunk.hxml b/haxe_libraries/tink_chunk.hxml new file mode 100644 index 00000000..e5e5ba9f --- /dev/null +++ b/haxe_libraries/tink_chunk.hxml @@ -0,0 +1,3 @@ +-D tink_chunk=0.2.0 +# @install: lix --silent download "haxelib:/tink_chunk#0.2.0" into tink_chunk/0.2.0/haxelib +-cp ${HAXE_LIBCACHE}/tink_chunk/0.2.0/haxelib/src diff --git a/haxe_libraries/tink_cli.hxml b/haxe_libraries/tink_cli.hxml new file mode 100644 index 00000000..2ad9ac04 --- /dev/null +++ b/haxe_libraries/tink_cli.hxml @@ -0,0 +1,8 @@ +-D tink_cli=0.4.1 +# @install: lix --silent download "haxelib:/tink_cli#0.4.1" into tink_cli/0.4.1/haxelib +-lib tink_io +-lib tink_stringly +-lib tink_macro +-cp ${HAXE_LIBCACHE}/tink_cli/0.4.1/haxelib/src +# Make sure docs are generated +-D use-rtti-doc \ No newline at end of file diff --git a/haxe_libraries/tink_core.hxml b/haxe_libraries/tink_core.hxml new file mode 100644 index 00000000..37a0d96a --- /dev/null +++ b/haxe_libraries/tink_core.hxml @@ -0,0 +1,3 @@ +# @install: lix --silent download "haxelib:/tink_core#2.1.0" into tink_core/2.1.0/haxelib +-cp ${HAXE_LIBCACHE}/tink_core/2.1.0/haxelib/src +-D tink_core=2.1.0 \ No newline at end of file diff --git a/haxe_libraries/tink_io.hxml b/haxe_libraries/tink_io.hxml new file mode 100644 index 00000000..b923110e --- /dev/null +++ b/haxe_libraries/tink_io.hxml @@ -0,0 +1,5 @@ +-D tink_io=0.6.2 +# @install: lix --silent download "haxelib:/tink_io#0.6.2" into tink_io/0.6.2/haxelib +-lib tink_chunk +-lib tink_streams +-cp ${HAXE_LIBCACHE}/tink_io/0.6.2/haxelib/src diff --git a/haxe_libraries/tink_macro.hxml b/haxe_libraries/tink_macro.hxml new file mode 100644 index 00000000..a9e19b1f --- /dev/null +++ b/haxe_libraries/tink_macro.hxml @@ -0,0 +1,4 @@ +# @install: lix --silent download "haxelib:/tink_macro#1.0.1" into tink_macro/1.0.1/haxelib +-lib tink_core +-cp ${HAXE_LIBCACHE}/tink_macro/1.0.1/haxelib/src +-D tink_macro=1.0.1 \ No newline at end of file diff --git a/haxe_libraries/tink_priority.hxml b/haxe_libraries/tink_priority.hxml new file mode 100644 index 00000000..98cfa803 --- /dev/null +++ b/haxe_libraries/tink_priority.hxml @@ -0,0 +1,3 @@ +-D tink_priority=0.1.3 +# @install: lix --silent download "gh://github.com/haxetink/tink_priority#ea736d31dc788aae703a2aa415c25d5b80d0e7d1" into tink_priority/0.1.3/github/ea736d31dc788aae703a2aa415c25d5b80d0e7d1 +-cp ${HAXE_LIBCACHE}/tink_priority/0.1.3/github/ea736d31dc788aae703a2aa415c25d5b80d0e7d1/src diff --git a/haxe_libraries/tink_streams.hxml b/haxe_libraries/tink_streams.hxml new file mode 100644 index 00000000..ae8ed277 --- /dev/null +++ b/haxe_libraries/tink_streams.hxml @@ -0,0 +1,6 @@ +-D tink_streams=0.3.2 +# @install: lix --silent download "haxelib:/tink_streams#0.3.2" into tink_streams/0.3.2/haxelib +-lib tink_core +-cp ${HAXE_LIBCACHE}/tink_streams/0.3.2/haxelib/src +# temp for development, delete this file when pure branch merged +-D pure \ No newline at end of file diff --git a/haxe_libraries/tink_stringly.hxml b/haxe_libraries/tink_stringly.hxml new file mode 100644 index 00000000..9cf46258 --- /dev/null +++ b/haxe_libraries/tink_stringly.hxml @@ -0,0 +1,4 @@ +-D tink_stringly=0.2.0 +# @install: lix --silent download "haxelib:/tink_stringly#0.2.0" into tink_stringly/0.2.0/haxelib +-lib tink_core +-cp ${HAXE_LIBCACHE}/tink_stringly/0.2.0/haxelib/src diff --git a/haxe_libraries/tink_syntaxhub.hxml b/haxe_libraries/tink_syntaxhub.hxml new file mode 100644 index 00000000..a0e37e31 --- /dev/null +++ b/haxe_libraries/tink_syntaxhub.hxml @@ -0,0 +1,4 @@ +-cp src +-D tink_syntaxhub +--macro tink.SyntaxHub.use() +-lib tink_priority \ No newline at end of file diff --git a/haxe_libraries/travix.hxml b/haxe_libraries/travix.hxml new file mode 100644 index 00000000..527629d7 --- /dev/null +++ b/haxe_libraries/travix.hxml @@ -0,0 +1,6 @@ +-D travix=0.12.2 +# @install: lix --silent download "gh://github.com/back2dos/travix#7da3bf96717b52bf3c7e5d2273bf927a8cd7aeb5" into travix/0.12.2/github/7da3bf96717b52bf3c7e5d2273bf927a8cd7aeb5 +# @post-install: cd ${HAXE_LIBCACHE}/travix/0.12.2/github/7da3bf96717b52bf3c7e5d2273bf927a8cd7aeb5 && haxe -cp src --run travix.PostDownload +# @run: haxelib run-dir travix ${HAXE_LIBCACHE}/travix/0.12.2/github/7da3bf96717b52bf3c7e5d2273bf927a8cd7aeb5 +-lib tink_cli +-cp ${HAXE_LIBCACHE}/travix/0.12.2/github/7da3bf96717b52bf3c7e5d2273bf927a8cd7aeb5/src diff --git a/haxelib.json b/haxelib.json new file mode 100644 index 00000000..9db5be40 --- /dev/null +++ b/haxelib.json @@ -0,0 +1,24 @@ +{ + "name": "tink_syntaxhub", + "description": "Hub for plugging in language extensions.", + "classPath": "src", + "dependencies": { + "tink_priority": "", + "tink_macro": "" + }, + "url": "https://github.com/haxetink/tink_syntaxhub", + "contributors": [ + "back2dos" + ], + "version": "0.6.0", + "releasenote": "Get main class from tink_macro, thus supporting main transforms on eval from Haxe 4.3 onwards.", + "tags": [ + "tink", + "cross", + "utility", + "macro", + "syntax", + "sugar" + ], + "license": "MIT" +} \ No newline at end of file diff --git a/src/tink/SyntaxHub.hx b/src/tink/SyntaxHub.hx new file mode 100644 index 00000000..ecfcd0a2 --- /dev/null +++ b/src/tink/SyntaxHub.hx @@ -0,0 +1,106 @@ +package tink; + +import haxe.macro.*; +import haxe.macro.Expr; +import haxe.ds.Option; + +import tink.macro.ClassBuilder; + +import tink.priority.Queue; +import tink.syntaxhub.*; + +using tink.CoreApi; +using haxe.macro.Tools; + +class SyntaxHub { + + static var MAIN:Null = null; + static var registered = false; + static function use() { + if (registered) { + return; + registered = true; + } + var args = Sys.args(); + + MAIN = tink.MacroApi.getMainClass().orNull(); + + FrontendContext.resetCache(); + classLevel.whenever(makeSyntax(exprLevel.appliedTo), exprLevel.id);//Apperently reinserting this every time is more reliable with the cache + Context.onTypeNotFound(FrontendContext.findType); + Compiler.addGlobalMetadata('', '@:build(tink.SyntaxHub.build())', true, true, false); + + } + + static function build():Array + return + switch Context.getLocalType() { + case null: null; + case TInst(_.get() => c, _): + + var builder = new ClassBuilder(); + + var changed = false; + + for (plugin in classLevel.getData()) + changed = plugin(builder) || changed; + + changed = applyMainTransform(builder) || changed; + + if (changed) + builder.export(builder.target.meta.has(':explain')); + else + null; + default: null; + } + + static public var classLevel(default, null) = new QueueBool>(); + static public var exprLevel(default, null) = new ExprLevelSyntax('tink.SyntaxHub::exprLevel'); + static public var transformMain(default, null) = new QueueExpr>(); + + static public var frontends(get, never):Queue; + + static inline function get_frontends() + return FrontendContext.plugins; + + static public function makeSyntax(rule:ClassBuilder->OptionExpr>):ClassBuilder->Bool + return function (ctx:ClassBuilder) + return switch rule(ctx) { + case Some(rule): + + function transform(f:Function) + if (f.expr != null) + f.expr = rule(f.expr); + + if (ctx.hasConstructor()) + ctx.getConstructor().onGenerate(transform); + + for (m in ctx) + switch m.kind { + case FFun(f): transform(f); + case FProp(_, _, _, e), FVar(_, e): + if (e != null) + e.expr = rule(e).expr;//TODO: it might be better to just create a new kind, rather than modifying the expression in place + } + + true; + case None: + false; + } + + static function applyMainTransform(c:ClassBuilder) + return + if (c.target.pack.concat([c.target.name]).join('.') == MAIN) { + var main = c.memberByName('main').sure(); + var f = main.getFunction().sure(); + + if (f.expr == null) + f.expr = macro @:pos(main.pos) { }; + + for (rule in transformMain) + f.expr = rule(f.expr); + + true; + } + else false; +} diff --git a/src/tink/syntaxhub/ExprLevelSyntax.hx b/src/tink/syntaxhub/ExprLevelSyntax.hx new file mode 100644 index 00000000..5a48debc --- /dev/null +++ b/src/tink/syntaxhub/ExprLevelSyntax.hx @@ -0,0 +1,58 @@ +package tink.syntaxhub; + +import haxe.ds.Option; +import haxe.macro.Expr; +import tink.macro.ClassBuilder; +import tink.priority.ID; + +import tink.priority.Queue; + +using tink.MacroApi; + +class ExprLevelSyntax { + public var inward(default, null):Queue; + public var outward(default, null):Queue; + public var id(default, null):ID; + + public function new(id) { + this.inward = new Queue(); + this.outward = new Queue(); + this.id = id; + } + + public function appliedTo(c:ClassBuilder):OptionExpr> { + function getRelevant(q:Queue) + return [for (p in q.getData()) if (p.appliesTo(c)) p]; + + var inward = getRelevant(inward), + outward = getRelevant(outward); + + if (inward.length + outward.length == 0) + return None; + + function apply(e:Expr) + return + if (e == null || e.expr == null) e; + else + switch e.expr { + case EMeta( { name: ':diet' }, _): e; + default: + for (rule in inward) + e = rule.apply(e); + + e = e.map(apply); + + for (rule in outward) + e = rule.apply(e); + + e; + } + + return Some(apply); + } +} + +typedef ExprLevelRule = { + function appliesTo(c:ClassBuilder):Bool; + function apply(e:Expr):Expr; +} \ No newline at end of file diff --git a/src/tink/syntaxhub/FrontendContext.hx b/src/tink/syntaxhub/FrontendContext.hx new file mode 100644 index 00000000..636d4b9e --- /dev/null +++ b/src/tink/syntaxhub/FrontendContext.hx @@ -0,0 +1,160 @@ +package tink.syntaxhub; + +import haxe.macro.Context; +import haxe.macro.Expr; + +import haxe.ds.Option; +import tink.core.Lazy; +import tink.priority.Queue; + +using sys.FileSystem; +using tink.MacroApi; + +enum IncludeKind { + KImport(i:ImportExpr); + KUsing(u:TypePath); +} + +class FrontendContext { + var types:Array; + public var name(default, null):String; + public var pack(default, null):Array; + + var dependencies:Array; + var includes:Array<{ kind: IncludeKind, pos:Position }>; + + function new(name, pack) { + types = []; + dependencies = []; + includes = []; + + this.name = name; + this.pack = pack; + } + + public function getType(?name:String, ?orCreate:Lazy) { + if (name == null) + name = this.name; + + for (t in types) { + if (t.name == name) return t; + } + + var ret = + if (orCreate != null) orCreate.get(); + else macro class { }; + + ret.name = name; + ret.pack = this.pack; + + types.push(ret); + + return ret; + } + + public function addDependency(file:String) + this.dependencies.push(file); + + public function addImport(name:String, mode:ImportMode, pos:Position) + includes.push({ + pos: pos, + kind: KImport({ + mode: mode, + path: [for (p in name.split('.')) { + name: p, + pos: pos, + }] + }) + }); + + public function addUsing(name:String, pos:Position) + includes.push( { + pos: pos, + kind: KUsing(name.asTypePath()) + }); + + static public var plugins(default, null) = new Queue(); + + static function buildModule(pack:Array, name:String) { + var ret = new FrontendContext(name, pack); + + for (result in seekFile(pack, name, plugins.getData())) { + ret.addDependency(result.file); + result.plugin.parse(result.file, ret); + } + + return ret; + } + static public function seekFile(pack:Array, name:String, plugins:Iterable) { + var ret = []; + for (cp in Context.getClassPath()) { + var pack = pack.copy(); + pack.unshift(cp); + pack.push(name); + var fileName = haxe.io.Path.join(pack); + for (p in plugins) + for (ext in p.extensions()) { + var candidate = '$fileName.$ext'; + if (candidate.exists()) + ret.push({ file: candidate, plugin: p }); + } + } + return ret; + } + + static function moduleForType(name:String) { + if (name.indexOf('__impl') != -1 || plugins.getData().length == 0) return; + var pack = name.split('.'); + var tname = pack.pop(); + var actual = pack.concat(['__impl', tname]).join('.'); + cache[name] = { + pack: pack, + name: tname, + pos: Context.currentPos(), + fields: [], + kind: TDAlias(actual.asComplexType()) + } + var exists = + try { + Context.getType(actual); + true; + } + catch (e:Dynamic) false; + + if (!exists) { + var module = buildModule(pack, tname); + if (module.types.length == 0) { + cache[name] = null; // clean the entry, but not in a way we would try to build this again + return; + } + + var imports = [], + usings = []; + + for (d in module.includes) + switch d.kind { + case KImport(i): + imports.push(i); + case KUsing(u): + var ct = TPath(u); + (macro @:pos(d.pos) ([][0] : $ct)).typeof().sure(); + usings.push(u); + } + + Context.defineModule(actual, module.types, imports, usings); + for (d in module.dependencies) + Context.registerModuleDependency(actual, d); + } + } + + static var cache:Map; + static public function resetCache() + cache = new Map(); + + @:noDoc + static public function findType(name:String):TypeDefinition { + if (!cache.exists(name)) + moduleForType(name); + return cache[name]; + } +} diff --git a/src/tink/syntaxhub/FrontendPlugin.hx b/src/tink/syntaxhub/FrontendPlugin.hx new file mode 100644 index 00000000..fbdbf82d --- /dev/null +++ b/src/tink/syntaxhub/FrontendPlugin.hx @@ -0,0 +1,6 @@ +package tink.syntaxhub; + +interface FrontendPlugin { + function extensions():Iterator; + function parse(file:String, context:FrontendContext):Void; +} \ No newline at end of file diff --git a/tests.hxml b/tests.hxml new file mode 100644 index 00000000..2d11215b --- /dev/null +++ b/tests.hxml @@ -0,0 +1,10 @@ +-lib tink_core +-lib tink_priority +-lib tink_macro +-cp src +-cp tests +-main Main +extraParams.hxml +-dce full +--macro TxtFrontend.use() +--macro XmlFrontend.use() \ No newline at end of file diff --git a/tests/HelloWorld.txt b/tests/HelloWorld.txt new file mode 100644 index 00000000..323dcd04 --- /dev/null +++ b/tests/HelloWorld.txt @@ -0,0 +1 @@ +Hello, hello!!! \ No newline at end of file diff --git a/tests/HelloWorld.xml b/tests/HelloWorld.xml new file mode 100644 index 00000000..c11e9a98 --- /dev/null +++ b/tests/HelloWorld.xml @@ -0,0 +1,2 @@ + +<3 \ No newline at end of file diff --git a/tests/Main.hx b/tests/Main.hx new file mode 100644 index 00000000..61948ace --- /dev/null +++ b/tests/Main.hx @@ -0,0 +1,19 @@ +package; +import haxe.unit.TestCase; +import haxe.unit.TestRunner; + +class Main extends TestCase { + + function testTxt() + assertEquals('Hello, hello!!!', HelloWorld.TEXT); + + function testXml() + assertEquals('<3', HelloWorld.XML.firstElement().firstChild().nodeValue); + + static function main() { + var t = new TestRunner(); + t.add(new Main()); + t.run(); + } + +} \ No newline at end of file diff --git a/tests/TxtFrontend.hx b/tests/TxtFrontend.hx new file mode 100644 index 00000000..3bb6114c --- /dev/null +++ b/tests/TxtFrontend.hx @@ -0,0 +1,27 @@ +import tink.syntaxhub.*; + +import haxe.macro.Expr; +import haxe.macro.Context; + +class TxtFrontend implements FrontendPlugin { + + public function new() {} + + public function extensions() + return ['txt'].iterator(); + + public function parse(file:String, context:FrontendContext):Void { + + var text = sys.io.File.getContent(file); + var pos = Context.makePosition({ file: file, min: 0, max: text.length }); + + context.getType().fields.push({ + name: 'TEXT', + access: [AStatic, APublic], + kind: FProp('default', 'null', macro : String, macro $v{text}), + pos: pos, + }); + } + static function use() + tink.SyntaxHub.frontends.whenever(new TxtFrontend()); +} \ No newline at end of file diff --git a/tests/XmlFrontend.hx b/tests/XmlFrontend.hx new file mode 100644 index 00000000..f7c68498 --- /dev/null +++ b/tests/XmlFrontend.hx @@ -0,0 +1,33 @@ +package; + +import tink.syntaxhub.*; +import haxe.macro.Expr; +import haxe.macro.Context; + +class XmlFrontend implements FrontendPlugin { + + public function new() {} + + public function extensions() + return ['xml'].iterator(); + + public function parse(file:String, context:FrontendContext):Void { + + var text = sys.io.File.getContent(file); + var pos = Context.makePosition({ file: file, min: 0, max: text.length }); + + try + Xml.parse(text) + catch (e:Dynamic) + Context.error('Failed to parse $file because: $e', pos); + + context.getType().fields.push({ + name: 'XML', + access: [AStatic, APublic], + kind: FProp('default', 'null', macro : Xml, macro Xml.parse($v{text})), + pos: pos, + }); + } + static function use() + tink.SyntaxHub.frontends.whenever(new XmlFrontend()); +} \ No newline at end of file diff --git a/tink_syntaxhub.hxml b/tink_syntaxhub.hxml new file mode 100644 index 00000000..990e7b56 --- /dev/null +++ b/tink_syntaxhub.hxml @@ -0,0 +1,9 @@ +-lib tink_syntaxhub +-cp tests +-js bin/tinksyntaxhub.js +-D analyzer +-main Main +extraParams.hxml +-dce full +--macro TxtFrontend.use() +--macro XmlFrontend.use()