Code/Wasm/Text/Parser.cs
using System.Collections.Generic;
using WasmBox.Pixie;
using WasmBox.Pixie.Code;
using WasmBox.Pixie.Markup;

namespace WasmBox.Wasm.Text {
    /// <summary>
    /// A parser for the WebAssembly text format.
    /// </summary>
    public sealed partial class Parser {
        /// <summary>
        /// Parses a sequence of tokens as S-expressions.
        /// </summary>
        /// <param name="tokens">The tokens to parse.</param>
        /// <param name="log">A log to send errors to.</param>
        /// <returns>A list of parsed S-expressions.</returns>
        public static IReadOnlyList<SExpression> ParseAsSExpressions(IEnumerable<Lexer.Token> tokens, ILog log) {
            using (var enumerator = tokens.GetEnumerator()) {
                return ParseAsSExpressions(enumerator, log, false);
            }
        }

        /// <summary>
        /// Parses a sequence of tokens as S-expressions.
        /// </summary>
        /// <param name="tokens">The tokens to parse.</param>
        /// <param name="log">A log to send errors to.</param>
        /// <param name="isNested">Tells if this parsing action is a nested rather than a top-level action.</param>
        /// <returns>A list of parsed S-expressions.</returns>
        private static IReadOnlyList<SExpression> ParseAsSExpressions(IEnumerator<Lexer.Token> tokens, ILog log, bool isNested) {
            var results = new List<SExpression>();
            while (tokens.MoveNext()) {
                var token = tokens.Current;
                if (token.Kind == Lexer.TokenKind.LeftParenthesis) {
                    if (tokens.MoveNext()) {
                        var head = tokens.Current;
                        if (head.Kind != Lexer.TokenKind.Keyword) {
                            log.Log(
                                new LogEntry(
                                    Severity.Error,
                                    "expected a keyword",
                                    "all S-expressions should begin with a keyword, but this one doesn't.",
                                    new HighlightedSource(new SourceRegion(head.Span))));
                        }
                        var tail = ParseAsSExpressions(tokens, log, true);
                        if (tokens.Current.Kind != Lexer.TokenKind.RightParenthesis) {
                            log.Log(
                                new LogEntry(
                                    Severity.Error,
                                    "no closing parenthesis",
                                    "left parenthesis indicates the start of an S-expression, but that expression is never closed.",
                                    new HighlightedSource(new SourceRegion(token.Span))));
                        }
                        results.Add(SExpression.Create(head, tail));
                    }
                    else {
                        log.Log(
                            new LogEntry(
                                Severity.Error,
                                "no closing parenthesis",
                                "left parenthesis indicates the start of an S-expression, but the file ends immediately after.",
                                new HighlightedSource(new SourceRegion(token.Span))));
                    }
                }
                else if (token.Kind == Lexer.TokenKind.RightParenthesis) {
                    if (!isNested) {
                        log.Log(
                            new LogEntry(
                                Severity.Error,
                                "excess parenthesis",
                                "right parenthesis does not close a left parenthesis.",
                                new HighlightedSource(new SourceRegion(token.Span))));
                    }
                    break;
                }
                else {
                    results.Add(SExpression.Create(token));
                }
            }
            return results;
        }
    }
}