diff options
| -rw-r--r-- | feed.xml | 392 | ||||
| -rwxr-xr-x | genrss.sh | 34 |
2 files changed, 426 insertions, 0 deletions
diff --git a/feed.xml b/feed.xml new file mode 100644 index 0000000..d397808 --- /dev/null +++ b/feed.xml @@ -0,0 +1,392 @@ +<?xml version="1.0" encoding="UTF-8"?> +<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> +<channel> +<title>Marc's Programming Notes</title> +<link>https://marc.vertes.org/</link> +<description>A blog about programming</description> +<managingEditor>mvertes@free.fr</managingEditor> +<pubDate>Thu, 18 May 2023 18:53:56 +0200</pubDate> +<item> +<title>Yaegi Internals</title> +<link>https://marc.vertes.org/yaegi-internals/</link> +<description>Design and implementation of a Go interpreter</description> +<author>Marc Vertes</author> +<pubDate>Wed, 03 May 2023 12:00:00 +0200</pubDate> +<content:encoded><![CDATA[<h1 id="yaegi-internals">Yaegi internals</h1> +<p><a href="https://github.com/traefik/yaegi">Yaegi</a> is an +interpreter of the Go language written in Go. This project was started +in Traefik-Labs initially to provide a simple and practical embedded +plugin engine for the traefik reverse proxy. Now, more than 200 plugins +contributed by the community are listed on the public catalog at <a +href="https://plugins.traefik.io">plugins.traefik.io</a>. The use of +yaegi extends also to other domains, for example <a +href="https://github.com/xo/xo">databases</a>, <a +href="https://github.com/slok/sloth">observability</a>, <a +href="https://github.com/cyberark/kubesploit">container security</a> and +many <a +href="https://github.com/traefik/yaegi/network/dependents?package_id=UGFja2FnZS0yMjc1NTQ3MjIy">others</a>.</p> +<p>Yaegi is lean and mean, as it delivers in a single package, with no +external dependency, a complete Go interpreter, compliant with the <a +href="https://go.dev/ref/spec">Go specification</a>. Lean, but also +mean: its code is dense, complex, not always idiomatic, and sometimes +maybe hard to understand.</p> +<p>This document is here to address that. In the following, after +getting an overview, we look under the hood, explore the internals and +discuss the design. Our aim is to provide the essential insights, +clarify the architecture and the code organization. But first, the +overview.</p> +<h2 id="overview-of-architecture">Overview of architecture</h2> +<p>Let’s see what happens inside yaegi when one executes the following +line:</p> +<div class="sourceCode" id="cb1"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a>interp<span class="op">.</span>Eval<span class="op">(</span><span class="st">`print("hello", 2+3)`</span><span class="op">)</span></span></code></pre></div> +<p>The following figure 1 displays the main steps of evaluation:</p> +<figure> +<img src="yaegi_internals_fig1.drawio.svg" +alt="figure 1: steps of evaluation" /> +<figcaption aria-hidden="true">figure 1: steps of +evaluation</figcaption> +</figure> +<ol type="1"> +<li><p>The <em>scanner</em> (provided by the <a +href="https://pkg.go.dev/go/scanner">go/scanner</a> package) transforms +a stream of characters (the source code) into a stream of tokens, +through a <a +href="https://en.wikipedia.org/wiki/Lexical_analysis">lexical +analysis</a> step.</p></li> +<li><p>The <em>parser</em> (provided by the <a +href="https://pkg.go.dev/go/parser">go/parser</a> package) transforms +the stream of tokens into an <a +href="https://en.wikipedia.org/wiki/Abstract_syntax_tree">abstract +syntax tree</a> or AST, through a <a +href="https://en.wikipedia.org/wiki/Syntax_analysis">syntax analysis</a> +step.</p></li> +<li><p>The <em>analyser</em> (implemented in the <a +href="https://pkg.go.dev/github.com/traefik/yaegi/interp">yaegi/interp</a> +package) performs the checks and creation of type, constant, variable +and function symbols. It also computes the <a +href="https://en.wikipedia.org/wiki/Control-flow_graph">control-flow +graphs</a> and memory allocations for symbols, through <a +href="https://en.wikipedia.org/wiki/Semantic_analysis_(compilers)">semantic +analysis</a> steps. All those metadata are obtained from and stored to +the nodes of the AST, making it annotated.</p></li> +<li><p>The <em>generator</em> (implemented in the <a +href="https://pkg.go.dev/github.com/traefik/yaegi/interp">yaegi/interp</a> +package) reads the annotated AST and produces code intructions to be +executed, through a <a +href="https://en.wikipedia.org/wiki/Code_generation_%28compiler%29">code +generation</a> step.</p></li> +<li><p>The <em>executor</em> (implemented in the <a +href="https://pkg.go.dev/github.com/traefik/yaegi/interp">yaegi/interp</a> +package) runs the code instructions in the context of the +interpreter.</p></li> +</ol> +<p>The interpreter is designed as a simple compiler, except that the +code is generated into memory instead of object files, and with an +executor module to run the specific instruction format.</p> +<p>We won’t spend more details on the scanner and the parser, both +provided by the standard library, and instead examine directly the +analyser.</p> +<h2 id="semantic-analysis">Semantic analysis</h2> +<p>The analyser performs the semantic analysis of the program to +interpret. This is done in several steps, all consisting of reading from +and writing to the AST, so we first examine the details and dynamics of +our AST representation.</p> +<h3 id="ast-dynamics">AST dynamics</h3> +<p>Hereafter stands the most important data structure of any compiler, +interpreter or other language tool, and the function to use it +(extracted from <a +href="https://github.com/traefik/yaegi/blob/8de3add6faf471a807182c7b8198fe863debc9d8/interp/interp.go#L284-L296">here</a>).</p> +<div class="sourceCode" id="cb2"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="co">// node defines a node of a (abstract syntax) tree.</span></span> +<span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="kw">type</span> node <span class="kw">struct</span> <span class="op">{</span></span> +<span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a> <span class="co">// Node children</span></span> +<span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a> child <span class="op">[]*</span>node</span> +<span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a> <span class="co">// Node metadata</span></span> +<span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a> <span class="op">...</span></span> +<span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span> +<span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a></span> +<span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="co">// walk traverses AST n in depth first order, invoking in function</span></span> +<span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="co">// at node entry and out function at node exit.</span></span> +<span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a><span class="kw">func</span> <span class="op">(</span>n <span class="op">*</span>node<span class="op">)</span> walk<span class="op">(</span>in <span class="kw">func</span><span class="op">(</span>n <span class="op">*</span>node<span class="op">)</span> <span class="dt">bool</span><span class="op">,</span> out <span class="kw">func</span><span class="op">(</span>n <span class="op">*</span>node<span class="op">))</span> <span class="op">{</span></span> +<span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> in <span class="op">!=</span> <span class="ot">nil</span> <span class="op">&&</span> <span class="op">!</span>in<span class="op">(</span>n<span class="op">)</span> <span class="op">{</span></span> +<span id="cb2-13"><a href="#cb2-13" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span></span> +<span id="cb2-14"><a href="#cb2-14" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span> +<span id="cb2-15"><a href="#cb2-15" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _<span class="op">,</span> child <span class="op">:=</span> <span class="kw">range</span> n<span class="op">.</span>child <span class="op">{</span></span> +<span id="cb2-16"><a href="#cb2-16" aria-hidden="true" tabindex="-1"></a> child<span class="op">.</span>Walk<span class="op">(</span>in<span class="op">,</span> out<span class="op">)</span></span> +<span id="cb2-17"><a href="#cb2-17" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span> +<span id="cb2-18"><a href="#cb2-18" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> out <span class="op">!=</span> <span class="ot">nil</span> <span class="op">{</span></span> +<span id="cb2-19"><a href="#cb2-19" aria-hidden="true" tabindex="-1"></a> out<span class="op">(</span>n<span class="op">)</span></span> +<span id="cb2-20"><a href="#cb2-20" aria-hidden="true" tabindex="-1"></a> <span class="op">}</span></span> +<span id="cb2-21"><a href="#cb2-21" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span></code></pre></div> +<p>The above code is deceptively simple. As in many complex systems, an +important part of the signification is carried by the relationships +between the elements and the patterns they form. It’s easier to +understand it by displaying the corresponding graph and consider the +system as a whole. We can do that using a simple example:</p> +<div class="sourceCode" id="cb3"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a>a <span class="op">:=</span> <span class="dv">3</span></span> +<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> a <span class="op">></span> <span class="dv">2</span> <span class="op">{</span></span> +<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span><span class="op">(</span><span class="st">"ok"</span><span class="op">)</span></span> +<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="op">}</span></span> +<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span><span class="op">(</span><span class="st">"bye"</span><span class="op">)</span></span></code></pre></div> +<p>The corresponding AST is:</p> +<figure> +<img src="ex1_raw_ast.drawio.svg" alt="figure 2: a raw AST" /> +<figcaption aria-hidden="true">figure 2: a raw AST</figcaption> +</figure> +<p>This is the raw AST, with no annotations, as obtained from the +parser. Each node contains an index number (for labelling purpose only), +and the node type, computed by the parser from the set of Go grammar +rules (i.e. “stmt” for “list of <a +href="https://go.dev/ref/spec#Statements">statements</a>”, “call” for +“call <a href="https://go.dev/ref/spec#Expressions">expression</a>”, …). +We also recognize the source tokens as literal values in leaf +locations.</p> +<p>Walking the tree consists in visiting the nodes starting from the +root (node 1), in their numbering order (here from 1 to 15): depth first +(the children before the siblings) and from left to right. At each node, +a callback <code>in</code> is invoked at entry (pre-processing) and a +callback <code>out</code> at exit (post-processing).</p> +<p>When the <code>in</code> callback executes, only the information +computed in the sub-trees in the left of the node is available, in +addition to the pre-processing information computed in the node +ancestors. The <code>in</code> callback returns a boolean. If the result +is false, the node sub-tree is skipped, allowing to short-cut +processing, for example to avoid to dive in function bodies and process +only function signatures.</p> +<p>When the <code>out</code> callback executes, the results computed on +the whole descendant sub-trees are available, which is useful for +example to compute the size of a composite object defined accross nested +structures. In the absence of post-processing, multiple tree walks are +necessary to achieve the same result.</p> +<p>A semantic analysis step is therefore simply a tree walk with the +right callbacks. In the case of our interpreter, we have two tree walks +to perform: the globals and types analysis in <a +href="https://github.com/traefik/yaegi/blob/master/interp/gta.go">interp/gta.go</a> +and the control-flow graphs analysis in <a +href="https://github.com/traefik/yaegi/blob/master/interp/cfg.go">interp/cfg.go</a>. +In both files, notice the call to <code>root.Walk</code>.</p> +<p>Note: we have chosen to represent the AST as a uniform node structure +as opposed to the <a href="https://pkg.go.dev/go/ast#Node">ast.Node</a> +interface in the Go standard library, implemented by specialized types +for all the node kinds. The main reason is that the tree walk method <a +href="https://pkg.go.dev/go/ast#Inspect">ast.Inspect</a> only permits a +pre-processing callback, not a post-processing one, required for several +compiling steps. It also seemed simpler at the time to start with this +uniform structure, and we ended up sticking with it.</p> +<h3 id="globals-and-types-analysis">Globals and types analysis</h3> +<p>Our first operation on the AST is to check and register all the +components of the program declared at global level. This is a partial +analysis, concerned only about declarations and not function +implementations.</p> +<p>This step is necessary because in Go, at global level, symbols can be +used before being declared (as opposed to Go function bodies, or in C in +general, where use before declaration is simply forbidden in strict +mode).</p> +<p>Allowing out of order symbols is what permits the code to be +scattered arbitrarily amongst several files in packages without more +constraints. It is indeed an important feature to let the programer +organize her code as she wants.</p> +<p>This step, implemented in <a +href="https://github.com/traefik/yaegi/blob/master/interp/gta.go">interp/gta.go</a>, +consists in performing a tree walk with only a pre-processing callback +(no <code>out</code> function is passed). There are two +particularities:</p> +<p>The first is the multiple-pass iterative walk. Indeed, in a first +global pass, instead of failing with an error whenever an incomplete +definition is met, the reference to the failing sub-tree is kept in a +list of nodes to be retried, and the walk finishes going over the whole +tree. Then, all the problematic sub-trees are iteratively retried until +all the nodes have been defined, or as long as there is progress. That +is, if two subsequent iterations lead to the exact same state, it is a +hint that progress is not being made and it would result in an infinite +loop, at which point yaegi just stops with an error.</p> +<p>The second particularity is that despite being in a partial analysis +step, a full interpretation can still be necessary on an expression +sub-tree if this one serves to implement a global type definition. For +example if an array size is computed by an expression as in the +following valid Go declarations:</p> +<div class="sourceCode" id="cb4"><pre class="sourceCode go"><code class="sourceCode go"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="kw">const</span> <span class="op">(</span></span> +<span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a> prefix <span class="op">=</span> <span class="st">"/usr"</span></span> +<span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a> path <span class="op">=</span> prefix <span class="op">+</span> <span class="st">"/local/bin"</span></span> +<span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="op">)</span></span> +<span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="kw">var</span> a <span class="op">[</span><span class="bu">len</span><span class="op">(</span>prefix<span class="op">+</span>path<span class="op">)</span> <span class="op">+</span> <span class="dv">2</span><span class="op">]</span><span class="dt">int</span></span></code></pre></div> +<p>A paradox is that the compiler needs an interpreter to perform the +type analysis! Indeed, in the example above, <code>[16]int</code> +(because <code>len(prefix+path) + 2 = 16</code>) is a specific type in +itself, distinct from e.g. <code>[14]int</code>. Which means that even +though we are only at the types analysis phase we already must be able +to compute the <code>len(prefix+path) + 2</code> expression. In the C +language it is one of the roles of the <a +href="https://gcc.gnu.org/onlinedocs/cpp/">pre-processor</a>, which +means the compiler itself does not need to be able to achieve that. Here +in Go, the specification forces the compiler implementor to provide and +use early-on the mechanics involved above, which is usually called +constant folding optimisation. It is therefore implemented both within +the standard gc, and whithin yaegi. The same kind of approach is pushed +to its paroxysm in the <a href="https://ziglang.org">Zig language</a> +with its <a +href="https://ziglang.org/documentation/master/#comptime">comptime</a> +keyword.</p> +<h3 id="control-flow-graphs">Control-flow graphs</h3> +<p>After GTA, all the global symbols are properly defined no matter +their declaration order. We can now proceed with the full code analysis, +which will be performed by a single tree walk in <a +href="https://github.com/traefik/yaegi/blob/master/interp/cfg.go">interp/cfg.go</a>.</p> +<p>Both pre-processing and post-processing callbacks are provided to the +walk function. Despite being activated in a single pass, multiple kinds +of data processing are executed:</p> +<ul> +<li><p>Types checking and creation. Started in GTA, it is now completed +also in all function bodies.</p></li> +<li><p>Analysis of variable scoping: scope levels are opened in +pre-processing and closed in post-processing, as the nesting of scope +reflects the AST structure.</p></li> +<li><p>Precise computing of object sizes and locations.</p></li> +<li><p>Identification and ordering of actions.</p></li> +</ul> +<p>The last point is critical for code generation. It consists in the +production of control-flow graphs. CFGs are usually represented in the +form of an intermediate representation (IR), which really is a +simplified machine independent instruction set, as in the <a +href="https://gcc.gnu.org/onlinedocs/gccint/GIMPLE.html">GCC GIMPLE</a>, +the <a href="https://llvm.org/docs/LangRef.html">LLVM IR</a> or the <a +href="https://github.com/golang/go/blob/bf48163e8f2b604f3b9e83951e331cd11edd8495/src/cmd/compile/internal/ssa/README.md">SSA</a> +form in the Go compiler. In yaegi, no IR is produced, only AST +annotations are used.</p> +<p>Let’s use our previous example to explain:</p> +<figure> +<img src="ex1_ast_cfg.drawio.svg" alt="figure 3: CFG is in AST" /> +<figcaption aria-hidden="true">figure 3: CFG is in AST</figcaption> +</figure> +<p>In the AST, the nodes relevant to the CFG are the <em>action</em> +nodes (in blue), that is the nodes referring to an arithmetic or a logic +operation, a function call or a memory operation (assigning a variable, +accessing an array entry, …).</p> +<p>Building the CFG consists in identifying action nodes and then find +their successor (to be stored in node fields <code>tnext</code> and +<code>fnext</code>). An action node has one successor in the general +case (shown with a green arrow), or two if the action is associated to a +conditional branch (green arrow if the test is true, red arrow +otherwise).</p> +<p>The rules to determine the successor of an action node are inherent +to the properties of its neighbours (ancestors, siblings and +descendants). For example, in the <code>if</code> sub-tree (nodes 5 to +12), the first action to execute is the condition test, that is, the +first action in the condition sub-tree, here the node 6. This action +will have two alternative successors: one to execute if the test is +true, the other if not. The <em>true</em> successor will be the first +action in the second child sub-tree of the <code>if</code> node, +describing the <em>true</em> branch (this sub-tree root is node 9, and +first action 10). As there is no <code>if</code> <em>false</em> branch +in our example, the next action of the whole <code>if</code> sub-tree is +the first action in the <code>if</code> sibling sub-tree, here the node +13. This node will be therefore the <em>false</em> successor, the first +action to execute when the <code>if</code> condition fails. Finally the +node 13 is also the successor of the <em>true</em> branch, the node 10. +The corresponding implementation is located in a <a +href="https://github.com/traefik/yaegi/blob/8de3add6faf471a807182c7b8198fe863debc9d8/interp/cfg.go#L1608-L1624">block +of 16 lines</a> in the post-processing CFG callback. Note that the same +code also performs dead branch elimination and condition validity +checking. At this stage, in terms of Control Flow, our AST example can +now be seen as a simpler representation, such as the following.</p> +<figure> +<img src="ex1_cfg.drawio.svg" alt="figure 4: the same CFG isolated" /> +<figcaption aria-hidden="true">figure 4: the same CFG +isolated</figcaption> +</figure> +<p>In our example, the action nodes composing the CFG can do the +following kind of operations: - defining variables in memory and +assigning values to them - performing arithmetic or logical operations - +conditional branching - function calling</p> +<p>Adding the capacity to jump to a <em>backward</em> location (where +destination node index is inferior to source’s one, an arrow from right +to left), thus allowing <em>loops</em>, makes the action set to become +<a href="https://en.wikipedia.org/wiki/Turing_completeness">Turing +complete</a>, implementing a universal computing machine.</p> +<figure> +<img src="ex1_cfg_loop.drawio.svg" alt="figure 5: a CFG with a loop" /> +<figcaption aria-hidden="true">figure 5: a CFG with a loop</figcaption> +</figure> +<p>The character of universality here lies in the cyclic nature of the +control-flow graph (remark that <code>if</code> statement graphs, +although appearing cyclic, are not, because the conditional branches are +mutually exclusives).</p> +<p>This is not just theoretical. For example, forbidding backward jumps +was crucial in the design of the Linux <a +href="https://www.kernel.org/doc/html/latest/bpf/verifier.html">eBPF +verifier</a>, in order to let user provided (therefore untrusted) +snippets execute in a kernel system privileged environment and guarantee +no infinite loops.</p> +<h2 id="code-generation-and-execution">Code generation and +execution</h2> +<p>The compiler implemented in yaegi targets the Go runtime itself, not +a particular hardware architecture. For each action node in the CFG a +corresponding closure is generated. The main benefits are:</p> +<ul> +<li>Portability: the generated code runs on any platform where Go is +supported.</li> +<li>Interoperability: the objects produced by the interpreter are +directly usable by the host program in the form of reflect values.</li> +<li>The memory management in particular the garbage collector, is +provided by the runtime, and applies also to the values created by the +interpreter.</li> +<li>The support of runtime type safety, slices, maps, channels, +goroutines is also provided by the runtime.</li> +</ul> +<p>The action templates are located in <a +href="https://github.com/traefik/yaegi/blob/master/interp/run.go">interp/run.go</a> +and <a +href="https://github.com/traefik/yaegi/blob/master/interp/op.go">interp/op.go</a>. +Generating closures allows to optimize all the cases where a constant is +used (an operation involving a constant and a variable is cheaper and +faster than the same operation involving two variables). It also permits +to hard-code the control-flow graph, that is to pre-define the next +instruction to execute and avoid useless branch tests.</p> +<p>The pseudo architecture targeted by the interpreter is in effect a +virtual <a href="https://en.wikipedia.org/wiki/Stack_machine">stack +machine</a> where the memory is represented as slices of Go reflect +values, as shown in the following figure, and where the instructions are +represented directly by the set of action nodes (the CFG) in the AST. +Those atomic instructions, also called <em>builtins</em>, are sligthly +higher level than a real hardware instruction set, because they operate +directly on Go interfaces (more precisely their reflect representation), +hiding a lot of low level processing and subtleties provided by the Go +runtime.</p> +<figure> +<img src="frame1.drawio.svg" alt="figure 6: frame organization" /> +<figcaption aria-hidden="true">figure 6: frame organization</figcaption> +</figure> +<p>The memory management performed by the interpreter consists of +creating a global frame at a new session (the top of the stack), +populated with all global values (constants, types, variables and +functions). At each new interpreted function call, a new frame is pushed +on the stack, containing the values for all the return value, input +parameters and local variables of the function.</p> +<h2 id="conclusion">Conclusion</h2> +<p>We have described the general architecture of a Go interpreter, +reusing the existing Go scanner and parser. We have focused on the +semantic analysis, which is based on AST annotations, up to the +control-flow graph and code generation. This design leads to a +consistent and concise compiler suitable for an embedded interpreter. We +have also provided a succint overview of the virtual stack machine on +top of the Go runtime, leveraging on the reflection layer provided by +the Go standard library.</p> +<p>We can now evolve this design to address different target +architectures, for example a more efficient virtual machine, already in +the works.</p> +<p>Some parts of yaegi have not been detailed yet and will be addressed +in a next article:</p> +<ul> +<li>Integration with pre-compiled packages</li> +<li>Go Generics</li> +<li>Recursive types</li> +<li>Interfaces and methods</li> +<li>Virtualization and sandboxing</li> +<li>REPL and interactive use</li> +</ul> +<p>P.S. Thanks to <a href="https://twitter.com/@lejatorn"><span +class="citation" data-cites="lejatorn">@lejatorn</span></a> for his +feedback and suggestions on this post.</p> +<hr>From: Marc Vertes, 03 may 2023]]> +</item> +</channel> +</rss> diff --git a/genrss.sh b/genrss.sh new file mode 100755 index 0000000..7e5d3b3 --- /dev/null +++ b/genrss.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +. ./meta.sh +cat <<- EOT + <?xml version="1.0" encoding="UTF-8"?> + <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"> + <channel> + <title>$title</title> + <link>$link/</link> + <description>$description</description> + <managingEditor>mvertes@free.fr</managingEditor> + <pubDate>$(date -R)</pubDate> +EOT + +for d in *; do + [ -d "$d" ] || continue + cd $d + . ./meta.sh + cat <<- EOT + <item> + <title>$title</title> + <link>$link/$d/</link> + <description>$description</description> + <author>$author</author> + <pubDate>$date_rfc2822</pubDate> + <content:encoded><![CDATA[$(awk '/<h1 / {p=!p} p' index.html)]]> + </item> + EOT +done + +cat <<- EOT + </channel> + </rss> +EOT |
