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 }