Code/Wasm/Text/Assembler.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
using WasmBox.Pixie;
using WasmBox.Pixie.Code;
using WasmBox.Pixie.Markup;
using WasmBox.Wasm.Instructions;
using WasmBox.Wasm.Optimize;
namespace WasmBox.Wasm.Text {
/// <summary>
/// An assembler for the WebAssembly text format. Converts parsed WebAssembly text format
/// modules to in-memory WebAssembly binary format modules.
/// </summary>
public sealed class Assembler {
/// <summary>
/// Creates a WebAssembly assembler.
/// </summary>
/// <param name="log">A log to send diagnostics to.</param>
public Assembler(ILog log)
: this(log, DefaultModuleFieldAssemblers, DefaultPlainInstructionAssemblers) { }
/// <summary>
/// Creates a WebAssembly assembler.
/// </summary>
/// <param name="log">A log to send diagnostics to.</param>
/// <param name="moduleFieldAssemblers">
/// A mapping of module field keywords to module field assemblers.
/// </param>
/// <param name="plainInstructionAssemblers">
/// A mapping of instruction keywords to instruction assemblers.
/// </param>
public Assembler(
ILog log,
IReadOnlyDictionary<string, ModuleFieldAssembler> moduleFieldAssemblers,
IReadOnlyDictionary<string, PlainInstructionAssembler> plainInstructionAssemblers) {
this.Log = log;
this.ModuleFieldAssemblers = moduleFieldAssemblers;
this.PlainInstructionAssemblers = plainInstructionAssemblers;
}
/// <summary>
/// Gets the log that is used for reporting diagnostics.
/// </summary>
/// <value>A log.</value>
public ILog Log { get; private set; }
/// <summary>
/// Gets the module field assemblers this assembler uses to process
/// module fields.
/// </summary>
/// <value>A mapping of module field keywords to module field assemblers.</value>
public IReadOnlyDictionary<string, ModuleFieldAssembler> ModuleFieldAssemblers { get; private set; }
/// <summary>
/// Gets the module field assemblers this assembler uses to process
/// module fields.
/// </summary>
/// <value>A mapping of module field keywords to module field assemblers.</value>
public IReadOnlyDictionary<string, PlainInstructionAssembler> PlainInstructionAssemblers { get; private set; }
/// <summary>
/// Assembles an S-expression representing a module into a WebAssembly module.
/// </summary>
/// <param name="expression">The expression to assemble.</param>
/// <returns>An assembled module.</returns>
public WasmFile AssembleModule(SExpression expression) {
return AssembleModule(expression, out string moduleId);
}
/// <summary>
/// Assembles an S-expression representing a module into a WebAssembly module.
/// </summary>
/// <param name="expression">The expression to assemble.</param>
/// <param name="moduleIdOrNull">
/// The module's identifier if one is assigned to the module; otherwise, <c>null</c>.
/// </param>
/// <returns>An assembled module.</returns>
public WasmFile AssembleModule(SExpression expression, out string moduleIdOrNull) {
if (!expression.IsCallTo("module")) {
Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"top-level modules must be encoded as S-expressions that call ",
"module",
"."),
Highlight(expression)));
}
// Parse the module's label, if it has one.
var fields = expression.Tail;
moduleIdOrNull = AssembleLabelOrNull(ref fields);
if (fields.Count > 0 && fields[0].IsSpecificKeyword("binary")) {
// We encountered a binary module.
fields = fields.Skip(1).ToArray();
var data = AssembleDataString(fields, Log);
using (var stream = new MemoryStream(data)) {
return WasmFile.ReadBinary(stream);
}
}
var file = new WasmFile();
if (moduleIdOrNull != null) {
// We encountered a module name. Turn it into a name entry and then skip it
// for the purpose of module field analysis.
file.ModuleName = moduleIdOrNull;
}
// First scan ahead for types in the module.
var context = new ModuleContext(this);
var nonTypeFields = new List<SExpression>();
foreach (var field in fields) {
if (field.IsCallTo("type")) {
ModuleFieldAssemblers["type"](field, file, context);
}
else {
nonTypeFields.Add(field);
}
}
// Now assemble the module's other fields.
foreach (var field in nonTypeFields) {
ModuleFieldAssembler fieldAssembler;
if (!field.IsCall) {
Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"unexpected token; expected a module field.",
Highlight(expression)));
}
else if (ModuleFieldAssemblers.TryGetValue((string)field.Head.Value, out fieldAssembler)) {
fieldAssembler(field, file, context);
}
else {
Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unexpected module field type ",
(string)field.Head.Value,
"."),
Highlight(expression)));
}
}
context.ResolveIdentifiers(file);
return file;
}
/// <summary>
/// Assembles an S-expression representing a module into a WebAssembly module.
/// </summary>
/// <param name="tokens">A stream of tokens to parse and assemble.</param>
/// <returns>An assembled module.</returns>
public WasmFile AssembleModule(IEnumerable<Lexer.Token> tokens) {
var exprs = Parser.ParseAsSExpressions(tokens, Log);
if (exprs.Count == 0) {
Log.Log(
new LogEntry(
Severity.Error,
"nothing to assemble",
"input stream contains no S-expression that can be assembled into a module."));
return new WasmFile();
}
else if (exprs.Count != 1) {
Log.Log(
new LogEntry(
Severity.Error,
"multiple modules",
"input stream contains more than one S-expression to assemble into a module; expected just one.",
Highlight(exprs[1])));
}
return AssembleModule(exprs[0]);
}
/// <summary>
/// Assembles an S-expression representing a module into a WebAssembly module.
/// </summary>
/// <param name="document">A document to parse and assemble.</param>
/// <param name="fileName">The name of the file in which <paramref name="document"/> is saved.</param>
/// <returns>An assembled module.</returns>
public WasmFile AssembleModule(string document, string fileName = "<string>") {
return AssembleModule(Lexer.Tokenize(document, fileName));
}
/// <summary>
/// Assembles a top-level expression-style instruction.
/// </summary>
/// <param name="expression">The expression-style instruction to assemble.</param>
/// <param name="module">The module to which the expression-style instruction is scoped.</param>
/// <returns>A list of assembled instructions.</returns>
public IReadOnlyList<Instruction> AssembleInstructionExpression(SExpression expression, WasmFile module) {
var context = new InstructionContext(
new Dictionary<string, uint>(),
new ModuleContext(this),
module);
return AssembleExpressionInstruction(expression, context);
}
internal static HighlightedSource Highlight(Lexer.Token expression) {
return new HighlightedSource(new SourceRegion(expression.Span));
}
internal static HighlightedSource Highlight(SExpression expression) {
return Highlight(expression.Head);
}
/// <summary>
/// A type for module field assemblers.
/// </summary>
/// <param name="moduleField">A module field to assemble.</param>
/// <param name="module">The module that is being assembled.</param>
/// <param name="context">The module's assembly context.</param>
public delegate void ModuleFieldAssembler(
SExpression moduleField,
WasmFile module,
ModuleContext context);
/// <summary>
/// A type for plain instruction assemblers.
/// </summary>
/// <param name="keyword">The keyword expression that names the instruction.</param>
/// <param name="operands">
/// A nonempty list of S-expressions that represent instruction operands to assemble.
/// </param>
/// <param name="context">The module's assembly context.</param>
public delegate Instruction PlainInstructionAssembler(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context);
/// <summary>
/// Context that is used when assembling a module.
/// </summary>
public sealed class ModuleContext {
/// <summary>
/// Creates a module context.
/// </summary>
/// <param name="assembler">The assembler that gives rise to this conetxt.</param>
public ModuleContext(Assembler assembler) {
this.Assembler = assembler;
this.MemoryContext = IdentifierContext<MemoryType>.Create();
this.FunctionContext = IdentifierContext<LocalOrImportRef>.Create();
this.GlobalContext = IdentifierContext<LocalOrImportRef>.Create();
this.TableContext = IdentifierContext<LocalOrImportRef>.Create();
this.TypeContext = IdentifierContext<uint>.Create();
}
/// <summary>
/// Gets the identifier context for the module's memories.
/// </summary>
/// <value>An identifier context.</value>
public IdentifierContext<MemoryType> MemoryContext { get; private set; }
/// <summary>
/// Gets the identifier context for the module's functions.
/// </summary>
/// <value>An identifier context.</value>
public IdentifierContext<LocalOrImportRef> FunctionContext { get; private set; }
/// <summary>
/// Gets the identifier context for the module's globals.
/// </summary>
/// <value>An identifier context.</value>
public IdentifierContext<LocalOrImportRef> GlobalContext { get; private set; }
/// <summary>
/// Gets the identifier context for the module's tables.
/// </summary>
/// <value>An identifier context.</value>
public IdentifierContext<LocalOrImportRef> TableContext { get; private set; }
/// <summary>
/// Gets the identifier context for the module's types.
/// </summary>
/// <value>An identifier context.</value>
public IdentifierContext<uint> TypeContext { get; private set;}
/// <summary>
/// Gets the assembler that gives rise to this context.
/// </summary>
/// <value>An assembler.</value>
public Assembler Assembler { get; private set; }
/// <summary>
/// Gets the log used by the assembler and, by extension, this context.
/// </summary>
public ILog Log => Assembler.Log;
/// <summary>
/// Resolves any pending references in the module.
/// </summary>
/// <param name="module">The module for which this context was created.</param>
public void ResolveIdentifiers(WasmFile module) {
var importSection = module.GetFirstSectionOrNull<ImportSection>() ?? new ImportSection();
var memorySection = module.GetFirstSectionOrNull<MemorySection>() ?? new MemorySection();
var functionSection = module.GetFirstSectionOrNull<FunctionSection>() ?? new FunctionSection();
var globalSection = module.GetFirstSectionOrNull<GlobalSection>() ?? new GlobalSection();
var tableSection = module.GetFirstSectionOrNull<TableSection>() ?? new TableSection();
var memoryIndices = new Dictionary<MemoryType, uint>();
var functionIndices = new Dictionary<LocalOrImportRef, uint>();
var globalIndices = new Dictionary<LocalOrImportRef, uint>();
var tableIndices = new Dictionary<LocalOrImportRef, uint>();
for (int i = 0; i < importSection.Imports.Count; i++) {
var import = importSection.Imports[i];
if (import is ImportedMemory importedMemory) {
memoryIndices[importedMemory.Memory] = (uint)memoryIndices.Count;
}
else if (import is ImportedFunction importedFunction) {
functionIndices[new LocalOrImportRef(true, (uint)i)] = (uint)functionIndices.Count;
}
else if (import is ImportedGlobal importedGlobal) {
globalIndices[new LocalOrImportRef(true, (uint)i)] = (uint)globalIndices.Count;
}
else if (import is ImportedTable importedTable) {
tableIndices[new LocalOrImportRef(true, (uint)i)] = (uint)tableIndices.Count;
}
}
foreach (var memory in memorySection.Memories) {
memoryIndices[memory] = (uint)memoryIndices.Count;
}
for (int i = 0; i < functionSection.FunctionTypes.Count; i++) {
functionIndices[new LocalOrImportRef(false, (uint)i)] = (uint)functionIndices.Count;
}
for (int i = 0; i < globalSection.GlobalVariables.Count; i++) {
globalIndices[new LocalOrImportRef(false, (uint)i)] = (uint)globalIndices.Count;
}
for (int i = 0; i < tableSection.Tables.Count; i++) {
tableIndices[new LocalOrImportRef(false, (uint)i)] = (uint)tableIndices.Count;
}
// Resolve memory identifiers.
MemoryContext.ResolveAll(
Assembler.Log,
mem => memoryIndices[mem]);
// Resolve function identifiers.
FunctionContext.ResolveAll(
Assembler.Log,
func => functionIndices[func]);
// Resolve global identifiers.
GlobalContext.ResolveAll(
Assembler.Log,
global => globalIndices[global]);
// Resolve table identifiers.
TableContext.ResolveAll(
Assembler.Log,
table => tableIndices[table]);
// Resolve type identifiers.
TypeContext.ResolveAll(
Assembler.Log,
index => index);
}
}
/// <summary>
/// Context that is used when assembling an instruction.
/// </summary>
public sealed class InstructionContext {
private InstructionContext(
IReadOnlyDictionary<string, uint> namedLocalIndices,
string labelOrNull,
ModuleContext moduleContext,
WasmFile module,
InstructionContext parent) {
this.NamedLocalIndices = namedLocalIndices;
this.LabelOrNull = labelOrNull;
this.ModuleContext = moduleContext;
this.Module = module;
this.ParentOrNull = parent;
}
/// <summary>
/// Creates a top-level instruction context.
/// </summary>
/// <param name="namedLocalIndices">The instruction context's named local indices.</param>
/// <param name="moduleContext">A context for the module that analyzes the instruction.</param>
/// <param name="module">The module that analyzes the instruction.</param>
public InstructionContext(
IReadOnlyDictionary<string, uint> namedLocalIndices,
ModuleContext moduleContext,
WasmFile module)
: this(namedLocalIndices, null, moduleContext, module, null) { }
/// <summary>
/// Creates a child instruction context with a particular label.
/// </summary>
/// <param name="labelOrNull">A label that a break table can branch to.</param>
/// <param name="parent">A parent instruction context.</param>
public InstructionContext(
string labelOrNull,
InstructionContext parent)
: this(parent.NamedLocalIndices, labelOrNull, parent.ModuleContext, parent.Module, parent) { }
/// <summary>
/// Gets a mapping of local variable names to their indices.
/// </summary>
/// <value>A mapping of names to indices.</value>
public IReadOnlyDictionary<string, uint> NamedLocalIndices { get; private set; }
/// <summary>
/// Gets the enclosing module context.
/// </summary>
/// <value>A module context.</value>
public ModuleContext ModuleContext { get; private set; }
/// <summary>
/// Gets the module associated with the enclosing module context.
/// </summary>
/// <value>A module.</value>
public WasmFile Module { get; private set; }
/// <summary>
/// Gets this instruction context's label if it has one
/// and <c>null</c> otherwise.
/// </summary>
/// <value>A label or <c>null</c>.</value>
public string LabelOrNull { get; private set; }
/// <summary>
/// Tells if this instruction context has a label.
/// </summary>
public bool HasLabel => LabelOrNull != null;
/// <summary>
/// Gets this instruction context's parent context if it has one
/// and <c>null</c> otherwise.
/// </summary>
/// <value>An instruction context or <c>null</c>.</value>
public InstructionContext ParentOrNull { get; private set; }
/// <summary>
/// Tells if this instruction context has a parent context.
/// </summary>
public bool HasParent => ParentOrNull != null;
/// <summary>
/// Gets the log used by the assembler and, by extension, this context.
/// </summary>
public ILog Log => ModuleContext.Log;
}
/// <summary>
/// A reference to a function, global, table or memory that is either defined
/// locally or imported.
/// </summary>
public struct LocalOrImportRef {
/// <summary>
/// Creates a reference to a function, global, table or memory that is either defined
/// locally or imported.
/// </summary>
/// <param name="isImport">
/// Tells if the value referred to by this reference is an import.
/// </param>
/// <param name="indexInSection">
/// The intra-section index of the value being referred to.
/// </param>
public LocalOrImportRef(bool isImport, uint indexInSection) {
this.IsImport = isImport;
this.IndexInSection = indexInSection;
}
/// <summary>
/// Tells if the value referred to by this reference is an import.
/// </summary>
/// <value><c>true</c> if the value is an import; otherwise, <c>false</c>.</value>
public bool IsImport { get; private set; }
/// <summary>
/// Gets the intra-section index of the value being referred to.
/// </summary>
/// <value>An intra-section index.</value>
public uint IndexInSection { get; private set; }
}
/// <summary>
/// An identifier context, which maps identifiers to indices.
/// </summary>
public struct IdentifierContext<T> {
/// <summary>
/// Creates an empty identifier context.
/// </summary>
/// <returns>An identifier context.</returns>
public static IdentifierContext<T> Create() {
return new IdentifierContext<T>() {
identifierDefinitions = new Dictionary<string, T>(),
pendingIdentifierReferences = new List<KeyValuePair<Lexer.Token, Action<uint>>>()
};
}
private Dictionary<string, T> identifierDefinitions;
private List<KeyValuePair<Lexer.Token, Action<uint>>> pendingIdentifierReferences;
/// <summary>
/// Defines a new identifier.
/// </summary>
/// <param name="identifier">The identifier to define.</param>
/// <param name="value">The value identified by the identifier.</param>
/// <returns>
/// <c>true</c> if <paramref name="identifier"/> is non-null and there is no
/// previous definition of the identifier; otherwise, <c>false</c>.
/// </returns>
public bool Define(string identifier, T value) {
if (identifier == null || identifierDefinitions.ContainsKey(identifier)) {
return false;
}
else {
identifierDefinitions[identifier] = value;
return true;
}
}
/// <summary>
/// Introduces a new identifier use.
/// </summary>
/// <param name="token">A token that refers to an identifier or an index.</param>
/// <param name="patch">
/// An action that patches a user based on the index assigned to the token.
/// Will be executed once the module is fully assembled.
/// </param>
public void Use(Lexer.Token token, Action<uint> patch) {
pendingIdentifierReferences.Add(
new KeyValuePair<Lexer.Token, Action<uint>>(token, patch));
}
/// <summary>
/// Introduces a new identifier use.
/// </summary>
/// <param name="value">A value that will eventually be assigned an index.</param>
/// <param name="patch">
/// An action that patches a user based on the index assigned to the value.
/// Will be executed once the module is fully assembled.
/// </param>
public void Use(T value, Action<uint> patch) {
Use(Lexer.Token.Synthesize(value), patch);
}
/// <summary>
/// Tries to map an identifier back to its definition.
/// </summary>
/// <param name="identifier">An identifier to inspect.</param>
/// <param name="definition">A definition for <paramref name="identifier"/>, if one exists already.</param>
/// <returns>
/// <c>true</c> <paramref name="identifier"/> is defined; otherwise, <c>false</c>.
/// </returns>
public bool TryGetDefinition(string identifier, out T definition) {
return identifierDefinitions.TryGetValue(identifier, out definition);
}
/// <summary>
/// Resolves all pending references.
/// </summary>
/// <param name="log">A log to send diagnostics to.</param>
/// <param name="getIndex">A function that maps defined values to indices.</param>
public void ResolveAll(ILog log, Func<T, uint> getIndex) {
foreach (var pair in pendingIdentifierReferences) {
uint index;
if (TryResolve(pair.Key, getIndex, out index)) {
pair.Value(index);
}
else {
var id = (string)pair.Key.Value;
var suggested = NameSuggestion.SuggestName(id, identifierDefinitions.Keys);
log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold("identifier ", id, " does is undefined"),
suggested == null
? (MarkupNode)"."
: Quotation.QuoteEvenInBold("; did you mean ", suggested, "?"),
Highlight(pair.Key)));
}
}
pendingIdentifierReferences.Clear();
}
/// <summary>
/// Tries to map an identifier to its associated index.
/// </summary>
/// <param name="identifier">An identifier to inspect.</param>
/// <param name="getIndex">A function that maps defined values to indices.</param>
/// <param name="index">The associated index.</param>
/// <returns>
/// <c>true</c> if an index was found for <paramref name="identifier"/>; otherwise, <c>false</c>.
/// </returns>
private bool TryResolve(string identifier, Func<T, uint> getIndex, out uint index) {
T val;
if (identifierDefinitions.TryGetValue(identifier, out val)) {
index = getIndex(val);
return true;
}
else {
index = 0;
return false;
}
}
/// <summary>
/// Tries to map an identifier or index to its associated index.
/// </summary>
/// <param name="identifierOrIndex">An identifier or index to inspect.</param>
/// <param name="getIndex">A function that maps defined values to indices.</param>
/// <param name="index">The associated index.</param>
/// <returns>
/// <c>true</c> if an index was found for <paramref name="identifierOrIndex"/>; otherwise, <c>false</c>.
/// </returns>
private bool TryResolve(Lexer.Token identifierOrIndex, Func<T, uint> getIndex, out uint index) {
if (identifierOrIndex.Kind == Lexer.TokenKind.UnsignedInteger) {
index = (uint)(BigInteger)identifierOrIndex.Value;
return true;
}
else if (identifierOrIndex.Kind == Lexer.TokenKind.Identifier) {
return TryResolve((string)identifierOrIndex.Value, getIndex, out index);
}
else if (identifierOrIndex.Kind == Lexer.TokenKind.Synthetic
&& identifierOrIndex.Value is T) {
index = getIndex((T)identifierOrIndex.Value);
return true;
}
else {
index = 0;
return false;
}
}
}
/// <summary>
/// The default set of module field assemblers.
/// </summary>
public static readonly IReadOnlyDictionary<string, ModuleFieldAssembler> DefaultModuleFieldAssemblers =
new Dictionary<string, ModuleFieldAssembler>() {
["data"] = AssembleDataSegment,
["elem"] = AssembleElementSegment,
["export"] = AssembleExport,
["func"] = AssembleFunction,
["global"] = AssembleGlobal,
["import"] = AssembleImport,
["memory"] = AssembleMemory,
["start"] = AssembleStart,
["table"] = AssembleTable,
["type"] = AssembleType
};
/// <summary>
/// The default set of instruction assemblers.
/// </summary>
public static readonly IReadOnlyDictionary<string, PlainInstructionAssembler> DefaultPlainInstructionAssemblers;
private static readonly Dictionary<MemoryOperator, uint> naturalAlignments = new Dictionary<MemoryOperator, uint>() {
[Operators.Int32Load] = 4,
[Operators.Int64Load] = 8,
[Operators.Float32Load] = 4,
[Operators.Float64Load] = 8,
[Operators.Int32Load8U] = 1,
[Operators.Int32Load8S] = 1,
[Operators.Int32Load16U] = 2,
[Operators.Int32Load16S] = 2,
[Operators.Int64Load8U] = 1,
[Operators.Int64Load8S] = 1,
[Operators.Int64Load16U] = 2,
[Operators.Int64Load16S] = 2,
[Operators.Int64Load32U] = 4,
[Operators.Int64Load32S] = 4,
[Operators.Int32Store] = 4,
[Operators.Int64Store] = 8,
[Operators.Float32Store] = 4,
[Operators.Float64Store] = 8,
[Operators.Int32Store8] = 1,
[Operators.Int32Store16] = 2,
[Operators.Int64Store8] = 1,
[Operators.Int64Store16] = 2,
[Operators.Int64Store32] = 4
};
static Assembler() {
var insnAssemblers = new Dictionary<string, PlainInstructionAssembler>() {
["i32.const"] = AssembleConstInt32Instruction,
["i64.const"] = AssembleConstInt64Instruction,
["f32.const"] = AssembleConstFloat32Instruction,
["f64.const"] = AssembleConstFloat64Instruction,
["block"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleBlockOrLoop( Operators.Block, keyword, ref operands, context, true ),
["loop"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleBlockOrLoop( Operators.Loop, keyword, ref operands, context, true ),
["if"] = AssembleIfInstruction,
["local.get"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleLocalInstruction( Operators.GetLocal, keyword, ref operands, context ),
["local.set"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleLocalInstruction( Operators.SetLocal, keyword, ref operands, context ),
["local.tee"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleLocalInstruction( Operators.TeeLocal, keyword, ref operands, context ),
["global.get"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleGlobalInstruction( Operators.GetGlobal, keyword, ref operands, context ),
["global.set"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleGlobalInstruction( Operators.SetGlobal, keyword, ref operands, context ),
["call"] = AssembleCallInstruction,
["call_indirect"] = AssembleCallIndirectInstruction,
["br"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleBrInstruction( Operators.Br, keyword, ref operands, context ),
["br_if"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
AssembleBrInstruction( Operators.BrIf, keyword, ref operands, context ),
["br_table"] = AssembleBrTableInstruction,
["memory.size"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
Operators.SizeMemory.Create( 0 ),
["memory.grow"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
Operators.GrowMemory.Create( 0 ),
["data.drop"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
Operators.DropData.Create( AssembleUInt32( operands[0], context.ModuleContext ) ),
["memory.copy"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
Operators.CopyMemory.Create( 0, 0 ),
["memory.fill"] = ( SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context ) =>
Operators.FillMemory.Create( 0 ),
};
DefaultPlainInstructionAssemblers = insnAssemblers;
foreach (var op in Operators.AllOperators) {
if (op is NullaryOperator nullary) {
// Nullary operators have a fairly regular structure that is almost identical
// to their mnemonics as specified for the binary encoding.
// The only way in which they are different is that they do not include slashes.
// To accommodate this, we map binary encoding mnemonics to text format mnemonics like
// so:
//
// i32.add -> i32.add
// 𝚏3𝟸.𝚌𝚘𝚗𝚟𝚎𝚛𝚝_𝚞/𝚒𝟼𝟺 -> 𝚏𝟹𝟸.𝚌𝚘𝚗𝚟𝚎𝚛𝚝_𝚒𝟼𝟺_𝚞
// 𝚏𝟹𝟸.𝚍𝚎𝚖𝚘𝚝𝚎/𝚏𝟼𝟺 -> 𝚏𝟹𝟸.𝚍𝚎𝚖𝚘𝚝𝚎_𝚏𝟼𝟺
//
var mnemonic = nullary.Mnemonic;
var mnemonicAndType = mnemonic.Split(new[] { '/' }, 2);
if (mnemonicAndType.Length == 2) {
var mnemonicAndSuffix = mnemonicAndType[0].Split(new[] { '_' }, 2);
if (mnemonicAndSuffix.Length == 2) {
mnemonic = $"{mnemonicAndSuffix[0]}_{mnemonicAndType[1]}_{mnemonicAndSuffix[1]}";
}
else {
mnemonic = $"{mnemonicAndType[0]}_{mnemonicAndType[1]}";
}
}
if (nullary.DeclaringType != WasmType.Empty) {
mnemonic = $"{DumpHelpers.WasmTypeToString(nullary.DeclaringType)}.{mnemonic}";
}
insnAssemblers[mnemonic] = (SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context) =>
nullary.Create();
}
else if (op is MemoryOperator memOp) {
var mnemonic = $"{DumpHelpers.WasmTypeToString(memOp.DeclaringType)}.{memOp.Mnemonic}";
insnAssemblers[mnemonic] = (SExpression keyword, ref IReadOnlyList<SExpression> operands, InstructionContext context) =>
AssembleMemoryInstruction(memOp, keyword, ref operands, context);
}
}
}
private static Instruction AssembleMemoryInstruction(
MemoryOperator memoryOperator,
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
var offset = AssembleOptionalNamedUInt32(ref operands, "offset", 0, context);
uint alignVal;
if (operands.Count > 0) {
var alignExpr = operands[0];
var align = AssembleOptionalNamedUInt32(ref operands, "align", naturalAlignments[memoryOperator], context);
var alignLog2 = Math.Log(align, 2);
if (Math.Floor(alignLog2) != alignLog2) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"alignment ", align.ToString(), " is not a power of two.",
Highlight(alignExpr)));
}
alignVal = (uint)alignLog2;
}
else {
alignVal = (uint)Math.Log(naturalAlignments[memoryOperator], 2);
}
return memoryOperator.Create(alignVal, offset);
}
private static uint AssembleOptionalNamedUInt32(
ref IReadOnlyList<SExpression> operands,
string keyword,
uint defaultValue,
InstructionContext context) {
uint offset = defaultValue;
if (operands.Count > 0 && operands[0].IsKeyword && ((string)operands[0].Head.Value).StartsWith(keyword + "=", StringComparison.Ordinal)) {
var offsetValText = ((string)operands[0].Head.Value).Substring(keyword.Length + 1);
var offsetValTokens = Lexer.Tokenize(offsetValText).ToArray();
if (offsetValTokens.Length != 1 || offsetValTokens[0].Kind != Lexer.TokenKind.UnsignedInteger) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"text ", offsetValText, " after keyword ", keyword + "=", " is not an unsigned integer.",
Highlight(operands[0])));
}
else {
offset = AssembleUInt32(SExpression.Create(offsetValTokens[0]), context.ModuleContext);
}
operands = operands.Skip(1).ToArray();
}
return offset;
}
private static Instruction AssembleConstInt32Instruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
if (AssertPopImmediate(keyword, ref operands, context, out SExpression immediate)) {
return Operators.Int32Const.Create(AssembleSignlessInt32(immediate, context.ModuleContext));
}
else {
return Operators.Int32Const.Create(0);
}
}
private static Instruction AssembleConstInt64Instruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
if (AssertPopImmediate(keyword, ref operands, context, out SExpression immediate)) {
return Operators.Int64Const.Create(AssembleSignlessInt64(immediate, context.ModuleContext));
}
else {
return Operators.Int64Const.Create(0);
}
}
private static Instruction AssembleConstFloat32Instruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
if (AssertPopImmediate(keyword, ref operands, context, out SExpression immediate)) {
return Operators.Float32Const.Create(AssembleFloat32(immediate, context.ModuleContext));
}
else {
return Operators.Float32Const.Create(float.NaN);
}
}
private static Instruction AssembleConstFloat64Instruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
if (AssertPopImmediate(keyword, ref operands, context, out SExpression immediate)) {
return Operators.Float64Const.Create(AssembleFloat64(immediate, context.ModuleContext));
}
else {
return Operators.Float64Const.Create(double.NaN);
}
}
private static bool AssertPopImmediate(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context,
out SExpression immediate) {
return AssertPopImmediate(keyword, ref operands, context.Log, out immediate);
}
private static bool AssertPopImmediate(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
ModuleContext context,
out SExpression immediate) {
return AssertPopImmediate(keyword, ref operands, context.Log, out immediate);
}
private static bool AssertPopImmediate(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
ILog log,
out SExpression immediate) {
if (operands.Count == 0) {
log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"expected another immediate.",
Highlight(keyword)));
immediate = default(SExpression);
return false;
}
else {
immediate = operands[0];
operands = operands.Skip(1).ToArray();
return true;
}
}
private static void AssembleMemory(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
const string kind = "memory definition";
// Process the optional memory identifier.
var memory = new MemoryType(new ResizableLimits(0));
var tail = moduleField.Tail;
var memoryId = AssembleLabelOrNull(ref tail);
if (memoryId != null) {
context.MemoryContext.Define(memoryId, memory);
}
if (!AssertNonEmpty(moduleField, tail, kind, context)) {
return;
}
// Parse inline exports.
var exportNames = AssembleInlineExports(moduleField, ref tail, context);
if (!AssertNonEmpty(moduleField, tail, kind, context)) {
return;
}
foreach (var exportName in exportNames) {
AddExport(module, context.MemoryContext, memory, ExternalKind.Memory, exportName);
}
if (tail[0].IsCallTo("data")) {
var data = AssembleDataString(tail[0].Tail, context);
var pageCount = (uint)Math.Ceiling((double)data.Length / MemoryType.PageSize);
memory.Limits = new ResizableLimits(pageCount, pageCount);
module.AddMemory(memory);
var dataSegment = new DataSegment(0, new InitializerExpression(Operators.Int32Const.Create(0)), data);
context.MemoryContext.Use(memory, index => { dataSegment.MemoryIndex = index; });
module.AddDataSegment(dataSegment);
AssertEmpty(context, kind, tail.Skip(1));
}
else if (tail[0].IsCallTo("import")) {
var (moduleName, memoryName) = AssembleInlineImport(tail[0], context);
tail = tail.Skip(1).ToArray();
memory.Limits = AssembleLimits(moduleField, tail, context);
var import = new ImportedMemory(moduleName, memoryName, memory);
module.AddImport(import);
}
else {
memory.Limits = AssembleLimits(moduleField, tail, context);
module.AddMemory(memory);
}
}
private static IReadOnlyList<string> AssembleInlineExports(
SExpression moduleField,
ref IReadOnlyList<SExpression> tail,
ModuleContext context) {
var results = new List<string>();
while (tail.Count > 0 && tail[0].IsCallTo("export")) {
var exportExpr = tail[0];
tail = tail.Skip(1).ToArray();
if (!AssertElementCount(exportExpr, exportExpr.Tail, 1, context)) {
continue;
}
results.Add(AssembleString(exportExpr.Tail[0], context));
}
return results;
}
private static void AssembleDataSegment(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
const string kind = "data segment";
var tail = moduleField.Tail;
if (!AssertNonEmpty(moduleField, tail, kind, context)) {
return;
}
Lexer.Token memoryId;
if (tail[0].IsIdentifier || tail[0].Head.Kind == Lexer.TokenKind.UnsignedInteger) {
memoryId = AssembleIdentifierOrIndex(tail[0], context);
tail = tail.Skip(1).ToArray();
}
else {
memoryId = Lexer.Token.Synthesize(new BigInteger(0), Lexer.TokenKind.UnsignedInteger);
}
var offset = AssembleOffset(moduleField, ref tail, context, module);
var data = AssembleDataString(tail, context);
var segment = new DataSegment(0u, offset, data);
context.MemoryContext.Use(memoryId, index => segment.MemoryIndex = index);
module.AddDataSegment(segment);
}
private static void AssembleElementSegment(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
const string kind = "element segment";
var tail = moduleField.Tail;
if (!AssertNonEmpty(moduleField, tail, kind, context)) {
return;
}
Lexer.Token tableId;
if (tail[0].IsIdentifier || tail[0].Head.Kind == Lexer.TokenKind.UnsignedInteger) {
tableId = AssembleIdentifierOrIndex(tail[0], context);
tail = tail.Skip(1).ToArray();
}
else {
tableId = Lexer.Token.Synthesize(new BigInteger(0), Lexer.TokenKind.UnsignedInteger);
}
var offset = AssembleOffset(moduleField, ref tail, context, module);
var segment = new ElementSegment(0u, offset, Enumerable.Empty<uint>());
int elemIndex = 0;
for ( int i = 0; i < tail.Count; i++ ) {
if ( tail[i].Head.Kind == Lexer.TokenKind.Keyword ) {
var keyword = (string)tail[i].Head.Value;
switch ( keyword ) {
case "func":
case "funcref": // ?
continue;
case "extern":
case "externref": // ?
context.Log.Log(
new LogEntry(
Severity.Error,
"not implemented error",
"extern function references are not supported",
Highlight( tail[i] ) ) );
continue;
}
}
int listIndex = elemIndex;
segment.Elements.Add( 0 );
context.FunctionContext.Use(
AssembleIdentifierOrIndex( tail[i], context ),
funcIndex => segment.Elements[listIndex] = funcIndex );
elemIndex++;
}
context.TableContext.Use(tableId, index => segment.TableIndex = index);
module.AddElementSegment(segment);
}
private static InitializerExpression AssembleOffset(
SExpression moduleField,
ref IReadOnlyList<SExpression> tail,
ModuleContext context,
WasmFile module) {
if (tail.Count == 0) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"expected a memory offset, specified as an instruction.",
Highlight(moduleField)));
return new InitializerExpression(Operators.Int32Const.Create(0));
}
if (tail[0].IsCallTo("offset")) {
var result = AssembleInitializerExpression(tail[0].Tail, context, module);
tail = tail.Skip(1).ToArray();
return result;
}
else {
var insnContext = new InstructionContext(new Dictionary<string, uint>(), context, module);
return new InitializerExpression(AssembleInstruction(ref tail, insnContext));
}
}
private static void AssembleExport(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
var tail = moduleField.Tail;
if (!AssertElementCount(moduleField, tail, 2, context)
|| !AssertElementCount(tail[1], tail[1].Tail, 1, context)) {
return;
}
var exportName = AssembleString(tail[0], context);
var index = AssembleIdentifierOrIndex(tail[1].Tail[0], context);
if (tail[1].IsCallTo("memory")) {
AddExport(module, context.MemoryContext, index, ExternalKind.Memory, exportName);
}
else if (tail[1].IsCallTo("func")) {
AddExport(module, context.FunctionContext, index, ExternalKind.Function, exportName);
}
else if (tail[1].IsCallTo("table")) {
AddExport(module, context.TableContext, index, ExternalKind.Table, exportName);
}
else if (tail[1].IsCallTo("global")) {
AddExport(module, context.GlobalContext, index, ExternalKind.Global, exportName);
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unexpected expression in export definition; expected ",
"func", ",", "table", ",", "memory", " or ", "global", "."),
Highlight(tail[1])));
}
}
private static void AssembleImport(SExpression moduleField, WasmFile module, ModuleContext context) {
if (!AssertElementCount(moduleField, moduleField.Tail, 3, context)) {
return;
}
var moduleName = AssembleString(moduleField.Tail[0], context);
var importName = AssembleString(moduleField.Tail[1], context);
var importDesc = moduleField.Tail[2];
string importId = null;
var importTail = importDesc.Tail;
if (importDesc.Tail.Count > 0 && importDesc.Tail[0].IsIdentifier) {
importId = (string)importDesc.Tail[0].Head.Value;
importTail = importTail.Skip(1).ToArray();
}
if (importDesc.IsCallTo("memory")) {
if (!AssertNonEmpty(importDesc, importTail, "import", context)) {
return;
}
var memory = new MemoryType(AssembleLimits(importDesc, importTail, context));
module.AddImport(new ImportedMemory(moduleName, importName, memory));
context.MemoryContext.Define(importId, memory);
}
else if (importDesc.IsCallTo("func")) {
var type = AssembleTypeUse(importDesc, ref importTail, context, module, true);
var typeIndex = AddOrReuseFunctionType(type, module);
var importIndex = module.AddImport(new ImportedFunction(moduleName, importName, typeIndex));
context.FunctionContext.Define(importId, new LocalOrImportRef(true, importIndex));
AssertEmpty(context, "import", importTail);
}
else if (importDesc.IsCallTo("global")) {
var type = AssembleGlobalType(importTail[0], context);
var importIndex = module.AddImport(new ImportedGlobal(moduleName, importName, type));
context.GlobalContext.Define(importId, new LocalOrImportRef(true, importIndex));
AssertEmpty(context, "global", importTail.Skip(1).ToArray());
}
else if (importDesc.IsCallTo("table")) {
var type = AssembleTableType(importDesc, importTail, context);
var importIndex = module.AddImport(new ImportedTable(moduleName, importName, type));
context.TableContext.Define(importId, new LocalOrImportRef(true, importIndex));
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unexpected expression in import; expected ",
"func", ",", "table", ",", "memory", " or ", "global", "."),
Highlight(importDesc)));
}
}
private static void AssembleTable(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
var tail = moduleField.Tail;
var tableId = AssembleLabelOrNull(ref tail);
var exportNames = AssembleInlineExports(moduleField, ref tail, context);
if (!AssertNonEmpty(moduleField, tail, "table definition", context)) {
return;
}
LocalOrImportRef tableRef;
if (tail[0].IsCallTo("import")) {
var (moduleName, importName) = AssembleInlineImport(tail[0], context);
var table = AssembleTableType(moduleField, tail.Skip(1).ToArray(), context);
var tableIndex = module.AddImport(new ImportedTable(moduleName, importName, table));
tableRef = new LocalOrImportRef(true, tableIndex);
context.TableContext.Define(tableId, tableRef);
}
else if (tail[0].Head.Kind == Lexer.TokenKind.Keyword) {
var elemType = AssembleElemType(tail[0], context);
if (!AssertElementCount(moduleField, tail, 2, context)) {
return;
}
var elems = tail[1];
if (!elems.IsCallTo("elem")) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unexpected expression in initialized table; expected an ",
"elem", " expression."),
Highlight(elems)));
}
var elemSegment = new ElementSegment(
0,
new InitializerExpression(Operators.Int32Const.Create(0)),
Enumerable.Empty<uint>());
module.AddElementSegment(elemSegment);
for (int i = 0; i < elems.Tail.Count; i++) {
elemSegment.Elements.Add(0);
var functionId = AssembleIdentifierOrIndex(elems.Tail[i], context);
var j = i;
context.FunctionContext.Use(functionId, index => { elemSegment.Elements[j] = index; });
}
var table = new TableType(elemType, new ResizableLimits((uint)elemSegment.Elements.Count));
var tableIndex = module.AddTable(table);
tableRef = new LocalOrImportRef(false, tableIndex);
context.TableContext.Define(tableId, tableRef);
context.TableContext.Use(tableRef, index => { elemSegment.TableIndex = index; });
}
else {
var table = AssembleTableType(moduleField, tail, context);
var tableIndex = module.AddTable(table);
tableRef = new LocalOrImportRef(false, tableIndex);
context.TableContext.Define(tableId, tableRef);
}
foreach (var exportName in exportNames) {
AddExport(module, context.TableContext, tableRef, ExternalKind.Table, exportName);
}
}
private static void AssembleType(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
var tail = moduleField.Tail;
var typeId = AssembleLabelOrNull(ref tail);
if (!AssertPopImmediate(moduleField, ref tail, context, out SExpression funcTypeExpr)) {
return;
}
var funcTypeTail = funcTypeExpr.Tail;
var funcType = AssembleTypeUse(funcTypeExpr, ref funcTypeTail, context, module);
AssertEmpty(context, "type definition", tail);
var index = module.AddFunctionType(funcType);
context.TypeContext.Define(typeId, index);
}
private static void AssembleStart(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
if (!AssertElementCount(moduleField, moduleField.Tail, 1, context)) {
return;
}
var idOrIndex = AssembleIdentifierOrIndex(moduleField.Tail[0], context);
context.FunctionContext.Use(idOrIndex, index => module.StartFunctionIndex = index);
}
private static void AssembleFunction(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
var tail = moduleField.Tail;
var functionId = AssembleLabelOrNull(ref tail);
// Parse export names.
var exportNames = AssembleInlineExports(moduleField, ref tail, context);
LocalOrImportRef funcRef;
if (tail.Count > 0 && tail[0].IsCallTo("import")) {
// We encountered an inline function import.
var (moduleName, functionName) = AssembleInlineImport(tail[0], context);
tail = tail.Skip(1).ToArray();
var funType = AssembleTypeUse(moduleField, ref tail, context, module, true);
AssertEmpty(context, "function import", tail);
var index = module.AddImport(
new ImportedFunction(moduleName, functionName, AddOrReuseFunctionType(funType, module)));
funcRef = new LocalOrImportRef(true, index);
}
else {
// We're dealing with a regular function definition.
var localIdentifiers = new Dictionary<string, uint>();
var funType = AssembleTypeUse(moduleField, ref tail, context, module, true, localIdentifiers);
var locals = AssembleLocals(ref tail, localIdentifiers, context, "local", funType.ParameterTypes.Count);
var insnContext = new InstructionContext(localIdentifiers, context, module);
var insns = new List<Instruction>();
while (tail.Count > 0) {
insns.AddRange(AssembleInstruction(ref tail, insnContext));
}
var index = module.AddFunction(
AddOrReuseFunctionType(funType, module),
new FunctionBody(locals.Select(x => new LocalEntry(x, 1)), insns));
funcRef = new LocalOrImportRef(false, index);
}
context.FunctionContext.Define(functionId, funcRef);
// Add entries to the export section if necessary.
foreach (var name in exportNames) {
AddExport(module, context.FunctionContext, funcRef, ExternalKind.Function, name);
}
}
private static void AssembleGlobal(
SExpression moduleField,
WasmFile module,
ModuleContext context) {
var tail = moduleField.Tail;
var globalId = AssembleLabelOrNull(ref tail);
// Parse export names.
var exportNames = AssembleInlineExports(moduleField, ref tail, context);
LocalOrImportRef globalRef;
if (tail.Count > 0 && tail[0].IsCallTo("import")) {
// We encountered an inline global import.
var (moduleName, globalName) = AssembleInlineImport(tail[0], context);
tail = tail.Skip(1).ToArray();
var globalType = AssembleGlobalType(moduleField, ref tail, context);
AssertEmpty(context, "function import", tail);
var index = module.AddImport(
new ImportedGlobal(moduleName, globalName, globalType));
globalRef = new LocalOrImportRef(true, index);
}
else {
// We're dealing with a regular global definition.
var globalType = AssembleGlobalType(moduleField, ref tail, context);
var init = AssembleInitializerExpression(tail, context, module);
var index = module.AddGlobal(
new GlobalVariable(globalType, init));
globalRef = new LocalOrImportRef(false, index);
}
context.GlobalContext.Define(globalId, globalRef);
// Add entries to the export section if necessary.
foreach (var name in exportNames) {
AddExport(module, context.GlobalContext, globalRef, ExternalKind.Global, name);
}
}
private static InitializerExpression AssembleInitializerExpression(
IReadOnlyList<SExpression> expressions,
ModuleContext context,
WasmFile module) {
var insnContext = new InstructionContext(new Dictionary<string, uint>(), context, module);
return new InitializerExpression(expressions.SelectMany(x => AssembleExpressionInstruction(x, insnContext)));
}
internal static string AssembleLabelOrNull(ref IReadOnlyList<SExpression> tail) {
string result = null;
if (tail.Count > 0 && tail[0].IsIdentifier) {
result = (string)tail[0].Head.Value;
tail = tail.Skip(1).ToArray();
}
return result;
}
private static IReadOnlyList<Instruction> AssembleInstruction(
ref IReadOnlyList<SExpression> instruction,
InstructionContext context) {
var first = instruction[0];
if (first.IsKeyword) {
PlainInstructionAssembler assembler;
if (context.ModuleContext.Assembler.PlainInstructionAssemblers.TryGetValue(
(string)first.Head.Value,
out assembler)) {
instruction = instruction.Skip(1).ToArray();
return new[] { assembler(first, ref instruction, context) };
}
else {
context.ModuleContext.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unknown instruction keyword ",
first.Head.Span.Text,
"."),
Highlight(first)));
instruction = Array.Empty<SExpression>();
return Array.Empty<Instruction>();
}
}
else if (first.IsCall) {
instruction = instruction.Skip(1).ToArray();
return AssembleExpressionInstruction(first, context);
}
else {
context.ModuleContext.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected an instruction; got ",
first.Head.Span.Text,
" instead."),
Highlight(first)));
instruction = Array.Empty<SExpression>();
return Array.Empty<Instruction>();
}
}
private static IReadOnlyList<Instruction> AssembleExpressionInstruction(
SExpression first,
InstructionContext context) {
if (!first.IsCall) {
context.ModuleContext.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected an expression, that is, a parenthesized instruction; got token ",
first.Head.Span.Text,
" instead."),
Highlight(first)));
return Array.Empty<Instruction>();
}
// Calls can be 'block' or 'loop' instructions, which are
// superficial syntactic sugar. They can also be 'if' instructions
// or folded instructions, which require a tiny bit of additional processing.
var blockTail = first.Tail;
if (first.IsCallTo("block") || first.IsCallTo("loop")) {
return new[] {
AssembleBlockOrLoop(
first.IsCallTo("block") ? Operators.Block : Operators.Loop,
first,
ref blockTail,
context,
false)
};
}
else if (first.IsCallTo("if")) {
return AssembleIfExpression(first, ref blockTail, context);
}
else {
IReadOnlyList<SExpression> childTail = new[] { SExpression.Create(first.Head) }
.Concat(blockTail)
.ToArray();
var lastInstruction = AssembleInstruction(ref childTail, context);
return childTail
.SelectMany(x => AssembleExpressionInstruction(x, context))
.Concat(lastInstruction)
.ToArray();
}
}
private static IReadOnlyList<Instruction> AssembleIfExpression(
SExpression first,
ref IReadOnlyList<SExpression> blockTail,
InstructionContext context) {
var label = AssembleLabelOrNull(ref blockTail);
var resultType = AssembleBlockResultType(ref blockTail, context);
var childContext = new InstructionContext(label, context);
var foldedInsns = new List<Instruction>();
while (blockTail.Count > 0 && !blockTail[0].IsCallTo("then")) {
foldedInsns.AddRange(AssembleInstruction(ref blockTail, context));
}
if (blockTail.Count == 0) {
context.ModuleContext.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"if-then-else instruction does not have a ", "then", "clause."),
Highlight(first)));
return Array.Empty<Instruction>();
}
var thenTail = blockTail[0].Tail;
string endKw;
var thenBody = AssembleBlockContents(first, ref thenTail, childContext, out endKw);
if (blockTail.Count > 1) {
if (blockTail[1].IsCallTo("else")) {
var elseTail = blockTail[1].Tail;
var elseBody = AssembleBlockContents(first, ref elseTail, childContext, out endKw);
return foldedInsns.Concat(new[] { Operators.If.Create(resultType, thenBody, elseBody) }).ToArray();
}
else {
context.ModuleContext.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unexpected expression; expected either nothing or an ", "else", " clause."),
Highlight(blockTail[1])));
}
}
return foldedInsns.Concat(new[] { Operators.If.Create(resultType, thenBody, Array.Empty<Instruction>()) }).ToArray();
}
private static Instruction AssembleIfInstruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
var label = AssembleLabelOrNull(ref operands);
var resultType = AssembleBlockResultType(ref operands, context);
var childContext = new InstructionContext(label, context);
string endKw;
var thenBody = AssembleBlockContents(keyword, ref operands, childContext, out endKw, "else", "end");
if (endKw == "else") {
ExpectOptionalLabel(ref operands, context, label);
var elseBody = AssembleBlockContents(keyword, ref operands, childContext, out endKw, "end");
ExpectOptionalLabel(ref operands, context, label);
return Operators.If.Create(resultType, thenBody, elseBody);
}
else {
ExpectOptionalLabel(ref operands, context, label);
return Operators.If.Create(resultType, thenBody, Array.Empty<Instruction>());
}
}
private static void ExpectOptionalLabel(
ref IReadOnlyList<SExpression> operands,
InstructionContext context,
string expectedLabel) {
if (operands.Count == 0) {
return;
}
var labelExpr = operands[0];
var label = AssembleLabelOrNull(ref operands);
if (label != null && label != expectedLabel) {
context.ModuleContext.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unexpected label ",
label,
"; expected either no label or label ",
expectedLabel,
"."),
Highlight(labelExpr)));
}
}
private static Instruction AssembleBlockOrLoop(
BlockOperator blockOperator,
SExpression parent,
ref IReadOnlyList<SExpression> operands,
InstructionContext context,
bool requireEnd) {
var label = AssembleLabelOrNull(ref operands);
var resultType = AssembleBlockResultType(ref operands, context);
var childContext = new InstructionContext(label, context);
string endKw;
var insns = requireEnd
? AssembleBlockContents(parent, ref operands, childContext, out endKw, "end")
: AssembleBlockContents(parent, ref operands, childContext, out endKw);
if (requireEnd) {
ExpectOptionalLabel(ref operands, context, label);
}
return blockOperator.Create(resultType, insns);
}
private static List<Instruction> AssembleBlockContents(
SExpression parent,
ref IReadOnlyList<SExpression> operands,
InstructionContext context,
out string endKeywordFound,
params string[] endKeywords) {
var insns = new List<Instruction>();
endKeywordFound = null;
while (operands.Count > 0) {
var first = operands[0];
if (first.IsKeyword && endKeywords.Contains((string)first.Head.Value)) {
operands = operands.Skip(1).ToArray();
endKeywordFound = (string)first.Head.Value;
break;
}
else {
insns.AddRange(AssembleInstruction(ref operands, context));
}
}
if (endKeywords.Length > 0 && endKeywordFound == null) {
context.ModuleContext.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected instruction to be terminated by an ",
"end",
" keyword."),
Highlight(parent)));
}
return insns;
}
private static WasmType AssembleBlockResultType(ref IReadOnlyList<SExpression> operands, InstructionContext context) {
var resultType = WasmType.Empty;
if (operands.Count > 0
&& operands[0].IsCallTo("result")
&& AssertElementCount(operands[0], operands[0].Tail, 1, context.ModuleContext)) {
resultType = (WasmType)AssembleValueType(operands[0].Tail[0], context.ModuleContext);
operands = operands.Skip(1).ToArray();
}
return resultType;
}
private static Instruction AssembleLocalInstruction(
VarUInt32Operator localOperator,
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
SExpression idOrIndex;
if (AssertPopImmediate(keyword, ref operands, context, out idOrIndex)) {
if (idOrIndex.IsCall) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected a local identifier or index; got ",
idOrIndex.Head.Span.Text,
" expression instead."),
Highlight(idOrIndex)));
}
else if (idOrIndex.Head.Kind == Lexer.TokenKind.UnsignedInteger) {
return localOperator.Create(AssembleUInt32(idOrIndex, context.ModuleContext));
}
else if (idOrIndex.Head.Kind == Lexer.TokenKind.Identifier) {
var id = (string)idOrIndex.Head.Value;
if (context.NamedLocalIndices.TryGetValue(id, out uint index)) {
return localOperator.Create(index);
}
else {
// TODO: suggest a name? Pixie can do that for us.
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"local variable identifier ",
id,
" is not defined in this scope."),
Highlight(idOrIndex)));
}
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected a local identifier or index; got token ",
idOrIndex.Head.Span.Text,
" instead."),
Highlight(idOrIndex)));
}
}
return Operators.Nop.Create();
}
private static Instruction AssembleTableRefInstruction<T>(
VarUInt32Operator tableRefOperator,
string refKind,
Func<ModuleContext, IdentifierContext<T>> getIdentifierContext,
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
SExpression idOrIndex;
var result = tableRefOperator.Create(0);
if (AssertPopImmediate(keyword, ref operands, context, out idOrIndex)) {
var token = AssembleIdentifierOrIndex(idOrIndex, context.ModuleContext);
getIdentifierContext(context.ModuleContext).Use(token, index => {
result.Immediate = index;
});
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
$"expected a {refKind} identifier or index; got ",
idOrIndex.Head.Span.Text,
" instead."),
Highlight(idOrIndex)));
}
return result;
}
private static Instruction AssembleGlobalInstruction(
VarUInt32Operator globalOperator,
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
return AssembleTableRefInstruction(globalOperator, "global", c => c.GlobalContext, keyword, ref operands, context);
}
private static Instruction AssembleCallInstruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
return AssembleTableRefInstruction(Operators.Call, "function", c => c.FunctionContext, keyword, ref operands, context);
}
private static Instruction AssembleCallIndirectInstruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
var identifiers = new Dictionary<string, uint>();
var typeUse = AssembleTypeUse(keyword, ref operands, context.ModuleContext, context.Module, true, identifiers);
if (identifiers.Count > 0) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"indirect calls cannot bind names to their parameter declarations; offending parameter name: ",
identifiers.Keys.First(),
"."),
Highlight(keyword)));
}
return Operators.CallIndirect.Create(AddOrReuseFunctionType(typeUse, context.Module));
}
private static Instruction AssembleBrInstruction(
VarUInt32Operator brOperator,
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
SExpression idOrIndex;
if (AssertPopImmediate(keyword, ref operands, context, out idOrIndex)) {
var depth = AssembleLabelOrDepth(idOrIndex, context);
return brOperator.Create(depth);
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected a label or break depth; got ",
idOrIndex.Head.Span.Text,
" instead."),
Highlight(idOrIndex)));
return brOperator.Create(0);
}
}
private static Instruction AssembleBrTableInstruction(
SExpression keyword,
ref IReadOnlyList<SExpression> operands,
InstructionContext context) {
var depths = new List<uint>();
if (operands.Count == 0) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected a branch target identifier or integer as argument to ",
"br_table",
"; got nothing."),
Highlight(keyword)));
return Operators.Nop.Create();
}
int i = 0;
do {
var immediate = operands[i];
if (immediate.IsKeyword || immediate.IsCall) {
break;
}
depths.Add(AssembleLabelOrDepth(immediate, context));
i++;
} while (operands.Count > i);
operands = operands.Skip(i).ToArray();
return Operators.BrTable.Create(depths.Take(depths.Count - 1), depths[depths.Count - 1]);
}
private static uint AssembleLabelOrDepth(
SExpression labelOrDepth,
InstructionContext context) {
var token = AssembleIdentifierOrIndex(labelOrDepth, context.ModuleContext);
if (token.Kind == Lexer.TokenKind.UnsignedInteger) {
return AssembleUInt32(labelOrDepth, context.ModuleContext);
}
else {
var label = (string)token.Value;
// We can turn a label into a break depth by iteratively unwinding the chain
// of scopes until we find a scope with a label that matches the label we're
// looking for. The number of scopes we had to unwind then corresponds to the
// break depth.
uint depth = 0;
bool found = false;
var depthContext = context;
while (depthContext != null) {
if (depthContext.LabelOrNull == label) {
found = true;
break;
}
else {
// Pop a context and see if the next context is the one we're looking for.
depth++;
depthContext = depthContext.ParentOrNull;
}
}
if (found) {
return depth;
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"label ",
label,
" is not defined here."),
Highlight(labelOrDepth)));
return 0;
}
}
}
private static List<WasmValueType> AssembleLocals(
ref IReadOnlyList<SExpression> tail,
Dictionary<string, uint> localIdentifiers,
ModuleContext context,
string localKeyword,
int parameterCount) {
var locals = new List<WasmValueType>();
// Parse locals.
while (tail.Count > 0 && tail[0].IsCallTo(localKeyword)) {
var paramSpec = tail[0];
var paramTail = paramSpec.Tail;
if (paramTail.Count > 0 && paramTail[0].IsIdentifier) {
var id = (string)paramTail[0].Head.Value;
paramTail = paramTail.Skip(1).ToArray();
if (!AssertNonEmpty(paramSpec, paramTail, localKeyword, context)) {
continue;
}
var valType = AssembleValueType(paramTail[0], context);
locals.Add(valType);
paramTail = paramTail.Skip(1).ToArray();
AssertEmpty(context, localKeyword, paramTail);
if (localIdentifiers != null) {
localIdentifiers[id] = (uint)(parameterCount + locals.Count - 1);
}
}
else {
locals.AddRange(paramTail.Select(x => AssembleValueType(x, context)));
}
tail = tail.Skip(1).ToArray();
}
return locals;
}
private static TableType AssembleTableType(SExpression parent, IReadOnlyList<SExpression> tail, ModuleContext context) {
if (tail.Count < 2 || tail.Count > 3) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected a table type, that is, resizable limits followed by ",
"funcref",
", the table element type."),
Highlight(parent)));
return new TableType(WasmType.AnyFunc, new ResizableLimits(0));
}
var limits = AssembleLimits(parent, tail.Take(tail.Count - 1).ToArray(), context);
var elemType = AssembleElemType(tail[tail.Count - 1], context);
return new TableType(WasmType.AnyFunc, limits);
}
private static WasmType AssembleElemType(SExpression expression, ModuleContext context) {
if (!expression.IsSpecificKeyword("funcref")) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"unexpected table type expression; expected ",
"funcref", "."),
Highlight(expression)));
}
return WasmType.AnyFunc;
}
private static GlobalType AssembleGlobalType(SExpression expression, ModuleContext context) {
if (expression.IsCallTo("mut")) {
if (!AssertElementCount(expression, expression.Tail, 1, context)) {
return new GlobalType(WasmValueType.Int32, true);
}
return new GlobalType(AssembleValueType(expression.Tail[0], context), true);
}
else {
return new GlobalType(AssembleValueType(expression, context), false);
}
}
private static GlobalType AssembleGlobalType(SExpression parent, ref IReadOnlyList<SExpression> tail, ModuleContext context) {
if (tail.Count == 0) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"expected a global type.",
Highlight(parent)));
return new GlobalType(WasmValueType.Int32, true);
}
else {
var immediate = tail[0];
tail = tail.Skip(1).ToArray();
return AssembleGlobalType(immediate, context);
}
}
private static FunctionType AssembleTypeUse(
SExpression parent,
ref IReadOnlyList<SExpression> tail,
ModuleContext context,
WasmFile module,
bool allowTypeRef = false,
Dictionary<string, uint> parameterIdentifiers = null) {
var result = new FunctionType();
FunctionType referenceType = null;
if (allowTypeRef && tail.Count > 0 && tail[0].IsCallTo("type")) {
var typeRef = tail[0];
tail = tail.Skip(1).ToArray();
if (AssertElementCount(typeRef, typeRef.Tail, 1, context)) {
uint referenceTypeIndex;
if ((typeRef.Tail[0].IsIdentifier
&& context.TypeContext.TryGetDefinition((string)typeRef.Tail[0].Head.Value, out referenceTypeIndex))) {
}
else if (!typeRef.Tail[0].IsCall
&& typeRef.Tail[0].Head.Kind == Lexer.TokenKind.UnsignedInteger) {
referenceTypeIndex = AssembleUInt32(typeRef.Tail[0], context);
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"expected an identifier or unsigned integer.",
Highlight(typeRef.Tail[0])));
return result;
}
var funTypes = module.GetFirstSectionOrNull<TypeSection>();
if (referenceTypeIndex >= funTypes.FunctionTypes.Count) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"index ", referenceTypeIndex.ToString(), " does not correspond to a type."),
Highlight(typeRef.Tail[0])));
return result;
}
referenceType = funTypes.FunctionTypes[(int)referenceTypeIndex];
if (tail.Count == 0 || (!tail[0].IsCallTo("param") && !tail[0].IsCallTo("result"))) {
return referenceType;
}
}
}
// Parse parameters.
result.ParameterTypes.AddRange(AssembleLocals(ref tail, parameterIdentifiers, context, "param", 0));
// Parse results.
while (tail.Count > 0 && tail[0].IsCallTo("result")) {
var resultSpec = tail[0];
var resultTail = resultSpec.Tail;
result.ReturnTypes.AddRange(resultTail.Select(x => AssembleValueType(x, context)));
tail = tail.Skip(1).ToArray();
}
if (referenceType != null) {
if (ConstFunctionTypeComparer.Instance.Equals(referenceType, result)) {
return referenceType;
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected locally-defined type ",
result.ToString(),
" to equal previously-defined type ",
referenceType.ToString(),
"."),
Highlight(parent)));
}
}
return result;
}
private static uint AddOrReuseFunctionType(
FunctionType type,
WasmFile module) {
var sec = module.GetFirstSectionOrNull<TypeSection>();
if (sec == null) {
return module.AddFunctionType(type);
}
else {
var index = sec.FunctionTypes.FindIndex(x => ConstFunctionTypeComparer.Instance.Equals(type, x));
if (index < 0) {
return module.AddFunctionType(type);
}
else {
return (uint)index;
}
}
}
private static WasmValueType AssembleValueType(SExpression expression, ModuleContext context) {
if (expression.IsSpecificKeyword("i32")) {
return WasmValueType.Int32;
}
else if (expression.IsSpecificKeyword("i64")) {
return WasmValueType.Int64;
}
else if (expression.IsSpecificKeyword("f32")) {
return WasmValueType.Float32;
}
else if (expression.IsSpecificKeyword("f64")) {
return WasmValueType.Float64;
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"unexpected token",
Quotation.QuoteEvenInBold(
"unexpected a value type, that is, ",
"i32", ",", "i64", ",", "f32", " or ", "f64", "."),
Highlight(expression)));
return WasmValueType.Int32;
}
}
private static void AddExport<T>(
WasmFile module,
IdentifierContext<T> context,
T value,
ExternalKind kind,
string exportName) {
var export = new ExportedValue(exportName, kind, 0);
module.AddExport(export);
var exportSection = module.GetFirstSectionOrNull<ExportSection>();
int index = exportSection.Exports.Count - 1;
context.Use(
value,
i => { exportSection.Exports[index] = new ExportedValue(exportName, kind, i); });
}
private static void AddExport<T>(
WasmFile module,
IdentifierContext<T> context,
Lexer.Token identifier,
ExternalKind kind,
string exportName) {
var export = new ExportedValue(exportName, kind, 0);
module.AddExport(export);
var exportSection = module.GetFirstSectionOrNull<ExportSection>();
int index = exportSection.Exports.Count - 1;
context.Use(
identifier,
i => { exportSection.Exports[index] = new ExportedValue(exportName, kind, i); });
}
private static Lexer.Token AssembleIdentifierOrIndex(SExpression expression, ModuleContext context) {
if (expression.Head.Kind != Lexer.TokenKind.UnsignedInteger
&& expression.Head.Kind != Lexer.TokenKind.Identifier) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"expected an identifier or unsigned integer.",
Highlight(expression)));
}
return expression.Head;
}
private static (string, string) AssembleInlineImport(SExpression import, ModuleContext context) {
if (import.Tail.Count != 2) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"encountered ",
import.Tail.Count.ToString(),
" elements; expected exactly two names."),
Highlight(import)));
return ("", "");
}
else {
return (AssembleString(import.Tail[0], context), AssembleString(import.Tail[1], context));
}
}
private static string AssembleString(SExpression expression, ModuleContext context) {
return AssembleString(expression, context.Log);
}
internal static string AssembleString(SExpression expression, ILog log) {
return Encoding.UTF8.GetString(AssembleByteString(expression, log));
}
internal static byte[] AssembleByteString(SExpression expression, ILog log) {
if (expression.IsCall || expression.Head.Kind != Lexer.TokenKind.String) {
log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"expected a string literal."),
Highlight(expression)));
return Array.Empty<byte>();
}
else {
return (byte[])expression.Head.Value;
}
}
private static bool AssertElementCount(
SExpression expression,
IReadOnlyList<SExpression> tail,
int count,
ModuleContext context) {
if (tail.Count == count) {
return true;
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(
"encountered ",
tail.Count.ToString(),
" elements; expected exactly ", count.ToString(), "."),
Highlight(expression)));
return false;
}
}
private static bool AssertNonEmpty(
SExpression expression,
IReadOnlyList<SExpression> tail,
string kind,
ModuleContext context) {
if (tail.Count == 0) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold(kind + " is unexpectedly empty."),
Highlight(expression)));
return false;
}
else {
return true;
}
}
private static byte[] AssembleDataString(
IReadOnlyList<SExpression> tail,
ILog log) {
var results = new List<byte>();
foreach (var item in tail) {
results.AddRange(AssembleByteString(item, log));
}
return results.ToArray();
}
private static byte[] AssembleDataString(
IReadOnlyList<SExpression> tail,
ModuleContext context) {
return AssembleDataString(tail, context.Log);
}
private static void AssertEmpty(
ModuleContext context,
string kind,
IEnumerable<SExpression> tail) {
var tailArray = tail.ToArray();
if (tailArray.Length > 0) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold("", kind, " has an unexpected trailing expression."),
Highlight(tailArray[0])));
}
}
private static ResizableLimits AssembleLimits(SExpression parent, IReadOnlyList<SExpression> tail, ModuleContext context) {
if (tail.Count == 0) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"limits expression is empty.",
Highlight(parent)));
return new ResizableLimits(0);
}
var init = AssembleUInt32(tail[0], context);
if (tail.Count == 1) {
return new ResizableLimits(init);
}
if (tail.Count == 2) {
var max = AssembleUInt32(tail[1], context);
return new ResizableLimits(init, max);
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"limits expression contains more than two elements.",
Highlight(tail[2])));
return new ResizableLimits(0);
}
}
private static uint AssembleUInt32(
SExpression expression,
ModuleContext context) {
return AssembleInt(
expression,
context,
"32-bit unsigned integer",
new[] { Lexer.TokenKind.UnsignedInteger },
(kind, data) => data <= uint.MaxValue ? (uint)data : (uint?)null);
}
private static int AssembleSignlessInt32(
SExpression expression,
ModuleContext context) {
return AssembleInt<int>(
expression,
context,
"32-bit integer",
new[] { Lexer.TokenKind.UnsignedInteger, Lexer.TokenKind.SignedInteger },
(kind, data) => {
if (expression.Head.Kind == Lexer.TokenKind.UnsignedInteger && data <= uint.MaxValue) {
return (int)(uint)data;
}
else if (data >= int.MinValue && data <= int.MaxValue) {
return (int)data;
}
else {
return null;
}
});
}
private static long AssembleSignlessInt64(
SExpression expression,
ModuleContext context) {
return AssembleInt<long>(
expression,
context,
"64-bit integer",
new[] { Lexer.TokenKind.UnsignedInteger, Lexer.TokenKind.SignedInteger },
(kind, data) => {
if (expression.Head.Kind == Lexer.TokenKind.UnsignedInteger && data <= ulong.MaxValue) {
return (long)(ulong)data;
}
else if (data >= long.MinValue && data <= long.MaxValue) {
return (long)data;
}
else {
return null;
}
});
}
private static float AssembleFloat32(SExpression expression, ModuleContext context) {
if (!expression.IsCall) {
if (expression.Head.Kind == Lexer.TokenKind.Float) {
return (float)(FloatLiteral)expression.Head.Value;
}
else if (expression.Head.Kind == Lexer.TokenKind.UnsignedInteger
|| expression.Head.Kind == Lexer.TokenKind.SignedInteger) {
return (float)(BigInteger)expression.Head.Value;
}
}
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"expected a floating point number.",
Highlight(expression)));
return float.NaN;
}
private static double AssembleFloat64(
SExpression expression,
ModuleContext context) {
if (!expression.IsCall) {
if (expression.Head.Kind == Lexer.TokenKind.Float) {
return (double)(FloatLiteral)expression.Head.Value;
}
else if (expression.Head.Kind == Lexer.TokenKind.UnsignedInteger
|| expression.Head.Kind == Lexer.TokenKind.SignedInteger) {
return (double)(BigInteger)expression.Head.Value;
}
}
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
"expected a floating point number.",
Highlight(expression)));
return double.NaN;
}
private static T AssembleInt<T>(
SExpression expression,
ModuleContext context,
string description,
Lexer.TokenKind[] acceptableKinds,
Func<Lexer.TokenKind, BigInteger, T?> tryCoerceInt)
where T : struct {
if (expression.IsCall) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
$"expected a {description}; got a call.",
Highlight(expression)));
return default(T);
}
else if (!acceptableKinds.Contains(expression.Head.Kind)) {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
Quotation.QuoteEvenInBold($"expected a {description}; token ", expression.Head.Span.Text, "."),
Highlight(expression)));
return default(T);
}
else {
var data = (BigInteger)expression.Head.Value;
var coerced = tryCoerceInt(expression.Head.Kind, data);
if (coerced.HasValue) {
return coerced.Value;
}
else {
context.Log.Log(
new LogEntry(
Severity.Error,
"syntax error",
$"expected a {description}; got an integer that is out of range.",
Highlight(expression)));
return default(T);
}
}
}
}
}