1 module djinn.translation; 2 3 @safe: 4 5 import std.array; 6 import std.exception; 7 import std.typecons : Flag, Yes, No; 8 9 import djinn.parsing; 10 import djinn.types; 11 12 // Hopefully something like this will be in Phobos someday 13 import djinn : writeText; 14 15 package: 16 17 /** 18 Takes an array of tokens and writes D code to output 19 20 No.useMixins: handle includes eagerly at run time 21 Yes.useMixins: write mixin statements/expressions to include files when the D code is used with a mixin 22 */ 23 void translateTokens(Flag!"useMixins" use_mixins = Yes.useMixins)(Appender!string output, const(Token)[] tokens) 24 { 25 import std.algorithm : startsWith; 26 import std..string : stripLeft; 27 static struct Fragment 28 { 29 string text; 30 Pos pos; 31 } 32 auto fragments_app = appender!(Fragment[]); 33 void flushFragments() 34 { 35 auto fs = fragments_app[]; 36 if (!fs.empty) 37 { 38 // FIXME: group lines 39 if (fs.length == 1) 40 { 41 writeLineSequence(output, fs[0].pos); 42 output.writeText("output.writeText(", fs[0].text, ");\n"); 43 } 44 else 45 { 46 output.put("output.writeText(\n"); 47 foreach (i, f; fs) 48 { 49 writeLineSequence(output, f.pos); 50 output.writeText(f.text, i == fs.length-1 ? ");\n" : ",\n"); 51 } 52 } 53 fragments_app.clear(); 54 } 55 } 56 foreach (token; tokens) 57 { 58 with (TokenType) final switch (token.type) 59 { 60 case text: 61 if (!token.value.empty) fragments_app.put(Fragment(doubleQuote(token.value), token.pos)); 62 break; 63 64 case statements: 65 flushFragments(); 66 if (!token.value.empty) 67 { 68 writeLineSequence(output, token.pos); 69 output.put(token.value); 70 output.put("\n"); 71 } 72 break; 73 74 case expressions: 75 string value = token.value; 76 if (value.stripLeft.startsWith(`"`)) value = `format(` ~ value ~ ')'; 77 fragments_app.put(Fragment(value, token.pos)); 78 break; 79 80 case directive: 81 flushFragments(); 82 handleDirective!use_mixins(output, token); 83 } 84 } 85 flushFragments(); 86 } 87 88 private: 89 90 /** 91 Adds line markers for mapping generated code lines to Djinn source lines 92 93 https://dlang.org/spec/lex.html#special-token-sequence 94 */ 95 void writeLineSequence(Appender!string output, ref const(Pos) pos) 96 { 97 output.writeText("# line ", pos.getLineNum(), ` "`, pos.src.fname, "\"\n"); 98 } 99 100 void handleDirective(Flag!"useMixins" use_mixins = Yes.useMixins)(Appender!string output, ref const(Token) token) 101 { 102 import std.algorithm.iteration : filter, splitter; 103 import std.file : readText; 104 if (token.value == "EOF") return; 105 106 auto args = token.value.splitter.filter!(e => !e.empty); // TODO: support quoting 107 enforce(!args.empty, syntaxException("Empty [< directive", token.pos)); 108 const cmd = args.front; 109 args.popFront(); 110 111 void checkLength(size_t req_len) 112 { 113 import std.conv : text; 114 import std.range.primitives : walkLength; 115 const len = args.walkLength; 116 enforce(len == req_len, syntaxException(text(req_len, " arguments needed for directive \"", cmd, "\" (got ", len, ")"), token.pos)); 117 } 118 119 string getFname() 120 { 121 return args.front.resolvePathFrom(token.pos.src.fname); 122 } 123 124 switch (cmd) 125 { 126 case "include": 127 checkLength(1); 128 const fname = getFname(); 129 static if (use_mixins) 130 { 131 output.writeText("mixin(translate!", doubleQuote(fname), ");\n"); 132 } 133 else 134 { 135 const contents = readText(fname); 136 auto inc_src = new Source(fname, contents); 137 auto tokens = getTokens(inc_src); 138 translateTokens!use_mixins(output, tokens); 139 } 140 break; 141 142 case "rawinclude": 143 checkLength(1); 144 const fname = getFname(); 145 static if (use_mixins) 146 { 147 output.writeText("output.writeText(import(", doubleQuote(fname), "));\n"); 148 } 149 else 150 { 151 const contents = readText(fname); 152 output.writeText("output.writeText(", doubleQuote(contents), ");\n"); 153 } 154 break; 155 156 case "xlatinclude": 157 checkLength(1); 158 const fname = getFname(); 159 static if (use_mixins) 160 { 161 output.writeText("output.writeText(translate!", doubleQuote(fname), ");\n"); 162 } 163 else 164 { 165 const contents = readText(fname); 166 auto inc_src = new Source(fname, contents); 167 auto tokens = getTokens(inc_src); 168 output.put("output.put(q{\n"); 169 translateTokens!use_mixins(output, tokens); 170 output.put("});\n"); 171 } 172 break; 173 174 case "raw": 175 case "endraw": 176 break; 177 178 default: 179 throw syntaxException("unrecognised [< directive: " ~ cmd, token.pos.src); 180 } 181 } 182 183 /// Interprets a path relative to the path of the source it was found in 184 string resolvePathFrom(string target_path, string src_path) pure 185 { 186 import std.path; 187 if (src_path == "<STDIN>") return target_path; 188 return buildNormalizedPath(dirName(src_path), target_path); 189 } 190 191 pure 192 unittest 193 { 194 assert (resolvePathFrom("foo.dj", "<STDIN>") == "foo.dj"); 195 assert (resolvePathFrom("foo.dj", "bar.dj") == "foo.dj"); 196 assert (resolvePathFrom("foo.dj", "/x/bar.dj") == "/x/foo.dj"); 197 assert (resolvePathFrom("x/foo.dj", "bar.dj") == "x/foo.dj"); 198 assert (resolvePathFrom("x/foo.dj", "y/bar.dj") == "y/x/foo.dj"); 199 } 200 201 /// Returns the D double-quoted string literal version of the input string 202 string doubleQuote(string s) pure 203 { 204 import std.format : format; 205 import std.range : only; 206 return format("%(%s%)", only(s)); 207 } 208 209 pure 210 unittest 211 { 212 assert (doubleQuote(``) == `""`); 213 assert (doubleQuote(`x`) == `"x"`); 214 assert (doubleQuote(`"x"`) == `"\"x\""`); 215 }