diff --git a/src/ast.ts b/src/ast.ts
index 8f2d0f8..ea45c26 100644
--- a/src/ast.ts
+++ b/src/ast.ts
@@ -1,6 +1,7 @@
import { ErrorEmitted, LoadedFile, Span, unreachable } from "./error";
import { LitIntType } from "./lexer";
import { ComplexMap } from "./utils";
+import { Instr, ValType } from "./wasm/defs";
export type Phase = {
res: unknown;
@@ -308,6 +309,12 @@ export type ExprTupleLiteral
= {
fields: Expr
[];
};
+export type ExprInlineAsm = {
+ kind: "asm";
+ locals: ValType[];
+ instructions: Instr[];
+};
+
export type ExprError = {
kind: "error";
err: ErrorEmitted;
@@ -330,6 +337,7 @@ export type ExprKind
=
| ExprBreak
| ExprStructLiteral
| ExprTupleLiteral
+ | ExprInlineAsm
| ExprError;
export type Expr
= ExprKind
& {
@@ -485,6 +493,8 @@ export const BUILTINS = [
"__memory_grow",
"__i32_extend_to_i64_u",
"___transmute",
+ "___asm",
+ "__locals",
] as const;
export type BuiltinName = (typeof BUILTINS)[number];
@@ -920,6 +930,9 @@ export function superFoldExpr(
fields: expr.fields.map(folder.expr.bind(folder)),
};
}
+ case "asm": {
+ return { ...expr };
+ }
case "error": {
return { ...expr };
}
diff --git a/src/codegen.ts b/src/codegen.ts
index 499a48c..d16bcf6 100644
--- a/src/codegen.ts
+++ b/src/codegen.ts
@@ -118,9 +118,7 @@ function appendData(cx: Context, newData: Uint8Array): number {
const KNOWN_DEF_PATHS = [ALLOCATE_ITEM, DEALLOCATE_ITEM];
-function getKnownDefPaths(
- pkgs: Pkg[],
-): ComplexMap {
+function getKnownDefPaths(pkgs: Pkg[]): ComplexMap {
const knows = new ComplexMap();
const folder: Folder = {
@@ -145,9 +143,7 @@ function getKnownDefPaths(
},
};
- pkgs.forEach((pkg) =>
- pkg.rootItems.forEach((item) => folder.item(item)),
- );
+ pkgs.forEach((pkg) => pkg.rootItems.forEach((item) => folder.item(item)));
return knows;
}
@@ -380,16 +376,22 @@ function lowerFunc(cx: Context, func: ItemFunction) {
scratchLocals: new Map(),
};
- lowerExpr(fcx, wasmFunc.body, fcx.func.body);
+ const body = fcx.func.body;
+ if (body.kind === "asm") {
+ fcx.wasm.locals = body.locals;
+ fcx.wasm.body = body.instructions;
+ } else {
+ lowerExpr(fcx, wasmFunc.body, body);
- paramLocations.forEach((local) => {
- const refcount = needsRefcount(local.ty);
- if (refcount !== undefined) {
- // TODO: correctly deal with tuples
- loadVariable(wasmFunc.body, local);
- subRefcount(fcx, wasmFunc.body, refcount);
- }
- });
+ paramLocations.forEach((local) => {
+ const refcount = needsRefcount(local.ty);
+ if (refcount !== undefined) {
+ // TODO: correctly deal with tuples
+ loadVariable(wasmFunc.body, local);
+ subRefcount(fcx, wasmFunc.body, refcount);
+ }
+ });
+ }
const idx = fcx.cx.mod.funcs.length;
fcx.cx.mod.funcs.push(wasmFunc);
@@ -1084,6 +1086,9 @@ function lowerExpr(
expr.fields.forEach((field) => lowerExpr(fcx, instrs, field));
break;
}
+ case "asm": {
+ unreachable("asm");
+ }
case "error":
unreachable("codegen should never see errors");
default: {
diff --git a/src/printer.ts b/src/printer.ts
index 9209a60..408dafc 100644
--- a/src/printer.ts
+++ b/src/printer.ts
@@ -211,6 +211,12 @@ function printExpr(expr: Expr, indent: number): string {
.map((expr) => printExpr(expr, indent))
.join(", ")})`;
}
+ case "asm": {
+ return `___asm(___locals(${expr.locals
+ .map((valty) => `"${valty}"`)
+ // object object
+ .join(", ")}), ${expr.instructions.join(", ")})`;
+ }
case "error":
return "";
}
diff --git a/src/typeck/expr.ts b/src/typeck/expr.ts
index 34ab9c3..a9eb8cc 100644
--- a/src/typeck/expr.ts
+++ b/src/typeck/expr.ts
@@ -28,12 +28,10 @@ import {
} from "../ast";
import { CompilerError, ErrorEmitted, Span, unreachable } from "../error";
import { printTy } from "../printer";
+import { INSTRS, Instr, VALTYPES, ValType } from "../wasm/defs";
import { TypeckCtx, emitError, mkTyFn, tyError, tyErrorFrom } from "./base";
import { InferContext } from "./infer";
-import {
- lowerAstTy,
- typeOfItem,
-} from "./item";
+import { lowerAstTy, typeOfItem } from "./item";
export function exprError(err: ErrorEmitted, span: Span): Expr {
return {
@@ -129,6 +127,15 @@ export function checkBody(
checkExpr: () => unreachable(),
};
+ if (
+ body.kind === "call" &&
+ body.lhs.kind === "ident" &&
+ body.lhs.value.res.kind === "builtin" &&
+ body.lhs.value.res.name === "___asm"
+ ) {
+ return checkInlineAsm(cx, body, fnTy.returnTy);
+ }
+
const checker: Folder = {
...mkDefaultFolder(),
expr(expr): Expr {
@@ -503,6 +510,9 @@ export function checkBody(
return { ...expr, fields, ty };
}
+ case "asm": {
+ unreachable("asm expression doesn't exist before type checking");
+ }
case "error": {
return { ...expr, ty: tyErrorFrom(expr) };
}
@@ -530,6 +540,90 @@ export function checkBody(
return resolved;
}
+function checkInlineAsm(
+ cx: TypeckCtx,
+ body: Expr & ExprCall,
+ retTy: Ty,
+): Expr {
+ const err = (msg: string, span: Span): Expr =>
+ exprError(emitError(cx, new CompilerError(msg, span)), span);
+
+ const args = body.args;
+
+ if (
+ args.length < 1 ||
+ args[0].kind !== "call" ||
+ args[0].lhs.kind !== "ident" ||
+ args[0].lhs.value.res.kind !== "builtin" ||
+ args[0].lhs.value.res.name !== "__locals"
+ ) {
+ return err(
+ "inline assembly must have __locals() as first argument",
+ body.span,
+ );
+ }
+
+ const locals: ValType[] = [];
+ for (const local of args[0].args) {
+ const isValtype = (s: string): s is ValType =>
+ VALTYPES.includes(s as ValType);
+ if (
+ local.kind !== "literal" ||
+ local.value.kind !== "str" ||
+ !isValtype(local.value.value)
+ ) {
+ return err(
+ "inline assembly local must be string literal of value type",
+ local.span,
+ );
+ }
+ locals.push(local.value.value);
+ }
+
+ const instructions: Instr[] = [];
+ for (const expr of args.slice(1)) {
+ if (expr.kind !== "literal" || expr.value.kind !== "str") {
+ return err(
+ "inline assembly instruction must be string literal with instruction",
+ expr.span,
+ );
+ }
+ const text = expr.value.value;
+ const parts = text.split(" ");
+ const imms = parts.slice(1);
+ const wasmInstr = INSTRS.find((instrVal) => instrVal.name === parts[0]);
+ if (!wasmInstr) {
+ return err(`unknown instruction: ${parts[0]}`, expr.span);
+ }
+ if (wasmInstr.immediates === "select") {
+ throw new Error("todo: select");
+ } else if (wasmInstr.immediates === "memarg") {
+ throw new Error("todo: memarg");
+ } else {
+ if (imms.length !== wasmInstr.immediates.length) {
+ return err(
+ `mismatched immediate lengths, expected ${wasmInstr.immediates.length}, got ${imms.length}`,
+ expr.span,
+ );
+ }
+ if (wasmInstr.immediates.length > 1) {
+ throw new Error("todo: immediates");
+ }
+
+ if (wasmInstr.immediates.length === 0) {
+ instructions.push({ kind: wasmInstr.name } as Instr);
+ } else {
+ instructions.push({
+ kind: wasmInstr.name,
+ imm: Number(imms[0]),
+ } as Instr);
+ }
+ }
+ }
+
+ return { kind: "asm", locals, ty: retTy, instructions, span: body.span };
+}
+
function checkLValue(cx: TypeckCtx, expr: Expr) {
switch (expr.kind) {
case "ident":
@@ -641,21 +735,19 @@ function checkCall(
fcx: FuncCtx,
expr: ExprCall & Expr,
): Expr {
- if (
- expr.lhs.kind === "ident" &&
- expr.lhs.value.res.kind === "builtin" &&
- expr.lhs.value.res.name === "___transmute"
- ) {
- const ty = fcx.infcx.newVar();
- const args = expr.args.map((arg) => fcx.checkExpr(arg));
- const ret: Expr = {
- ...expr,
- lhs: { ...expr.lhs, ty: TY_UNIT },
- args,
- ty,
- };
+ if (expr.lhs.kind === "ident" && expr.lhs.value.res.kind === "builtin") {
+ if (expr.lhs.value.res.name === "___transmute") {
+ const ty = fcx.infcx.newVar();
+ const args = expr.args.map((arg) => fcx.checkExpr(arg));
+ const ret: Expr = {
+ ...expr,
+ lhs: { ...expr.lhs, ty: TY_UNIT },
+ args,
+ ty,
+ };
- return ret;
+ return ret;
+ }
}
const lhs = fcx.checkExpr(expr.lhs);
diff --git a/src/wasm/defs.ts b/src/wasm/defs.ts
index 1cb8fcd..aa917ca 100644
--- a/src/wasm/defs.ts
+++ b/src/wasm/defs.ts
@@ -19,6 +19,15 @@ export type Vectype = "v128";
export type Reftype = "funcref" | "externref";
export type ValType = Numtype | Vectype | Reftype;
+export const VALTYPES: ValType[] = [
+ "i32",
+ "i64",
+ "f32",
+ "f64",
+ "v128",
+ "funcref",
+ "externref",
+];
export type ResultType = ValType[];
@@ -66,11 +75,25 @@ export type Externtype =
// instructions
+// Value representations of the types for the assembler
+export type InstrValue = {
+ name: Instr["kind"];
+ immediates: ImmediateValue[] | "select" | "memarg";
+};
+type ImmediateValue = "i32" | "i64" | "f32" | "f64" | "refkind";
+
+const ins = (name: Instr["kind"], immediates: InstrValue["immediates"]) => ({
+ name,
+ immediates,
+});
+
// . numeric
export type BitWidth = "32" | "64";
+const BIT_WIDTHS: BitWidth[] = ["32", "64"];
export type Sign = "u" | "s";
+const SIGNS: Sign[] = ["u", "s"];
export type NumericInstr =
| { kind: "i32.const"; imm: bigint }
@@ -86,7 +109,7 @@ export type NumericInstr =
| { kind: `f${BitWidth}.${FRelOp}` }
| { kind: `i${BitWidth}.extend8_s` }
| { kind: `i${BitWidth}.extend16_s` }
- | { kind: `i64.extend32_s` }
+ | { kind: "i64.extend32_s" }
| { kind: "i32.wrap_i64" }
| { kind: `i64.extend_i32_${Sign}` }
| { kind: `i${BitWidth}.trunc_f${BitWidth}_${Sign}` }
@@ -98,6 +121,7 @@ export type NumericInstr =
| { kind: "f32.reinterpret_i32" | "f64.reinterpret_i64" };
export type IUnOp = "clz" | "ctz" | "popcnt";
+const I_UN_OPS: IUnOp[] = ["clz", "ctz", "popcnt"];
export type IBinOp =
| "add"
@@ -112,19 +136,47 @@ export type IBinOp =
| `shr_${Sign}`
| "rotl"
| "rotr";
+const I_BIN_OPS: IBinOp[] = [
+ "add",
+ "sub",
+ "mul",
+ "and",
+ "or",
+ "xor",
+ "shl",
+ "rotl",
+ "rotr",
+ ...SIGNS.flatMap((sign): IBinOp[] => [
+ `div_${sign}`,
+ `rem_${sign}`,
+ `shr_${sign}`,
+ ]),
+];
-export type FUnOp =
- | "abs"
- | "neg"
- | "sqrt"
- | "ceil"
- | "floor"
- | "trunc"
- | "nearest";
+const F_UN_OPS = [
+ "abs",
+ "neg",
+ "sqrt",
+ "ceil",
+ "floor",
+ "trunc",
+ "nearest",
+] as const;
+export type FUnOp = (typeof F_UN_OPS)[number];
-export type FBinOp = "add" | "sub" | "mul" | "div" | "min" | "max" | "copysign";
+const F_BIN_OPS = [
+ "add",
+ "sub",
+ "mul",
+ "div",
+ "min",
+ "max",
+ "copysign",
+] as const;
+export type FBinOp = (typeof F_BIN_OPS)[number];
-export type ITestOp = "eqz";
+const I_TEST_OPS = ["eqz"] as const;
+export type ITestOp = (typeof I_TEST_OPS)[number];
export type IRelOp =
| "eq"
@@ -133,12 +185,62 @@ export type IRelOp =
| `gt_${Sign}`
| `le_${Sign}`
| `ge_${Sign}`;
+const I_REL_OPS: IRelOp[] = [
+ "eq",
+ "ne",
+ ...SIGNS.flatMap((sign): IRelOp[] => [
+ `lt_${sign}`,
+ `gt_${sign}`,
+ `le_${sign}`,
+ `ge_${sign}`,
+ ]),
+];
-export type FRelOp = "eq" | "ne" | "lt" | "gt" | "le" | "ge";
+const F_REL_OPS = ["eq", "ne", "lt", "gt", "le", "ge"] as const;
+export type FRelOp = (typeof F_REL_OPS)[number];
+
+const NO_IMM_NUMERIC_INSTRS: NumericInstr["kind"][] = [
+ ...BIT_WIDTHS.flatMap((bitWidth): NumericInstr["kind"][] => [
+ ...I_UN_OPS.map((op): NumericInstr["kind"] => `i${bitWidth}.${op}`),
+ ...F_UN_OPS.map((op): NumericInstr["kind"] => `f${bitWidth}.${op}`),
+ ...I_BIN_OPS.map((op): NumericInstr["kind"] => `i${bitWidth}.${op}`),
+ ...F_BIN_OPS.map((op): NumericInstr["kind"] => `f${bitWidth}.${op}`),
+ ...I_TEST_OPS.map((op): NumericInstr["kind"] => `i${bitWidth}.${op}`),
+ ...I_REL_OPS.map((op): NumericInstr["kind"] => `i${bitWidth}.${op}`),
+ ...F_REL_OPS.map((op): NumericInstr["kind"] => `f${bitWidth}.${op}`),
+ `i${bitWidth}.extend8_s`,
+ `i${bitWidth}.extend16_s`,
+ ...BIT_WIDTHS.flatMap((bitWidth2): NumericInstr["kind"][] =>
+ SIGNS.flatMap((sign): NumericInstr["kind"][] => [
+ `i${bitWidth}.trunc_f${bitWidth2}_${sign}`,
+ `i${bitWidth}.trunc_sat_f${bitWidth2}_${sign}`,
+ `f${bitWidth}.convert_i${bitWidth2}_${sign}`,
+ ]),
+ ),
+ ]),
+ "i64.extend32_s",
+ "i32.wrap_i64",
+ ...SIGNS.flatMap((sign): NumericInstr["kind"] => `i64.extend_i32_${sign}`),
+ "f32.demote_f64",
+ "f64.promote_f32",
+ "i32.reinterpret_f32",
+ "i64.reinterpret_f64",
+ "f32.reinterpret_i32",
+ "f64.reinterpret_i64",
+];
+const IMM_NUMERIC_INSTRS: InstrValue[] = (
+ ["i32", "i64", "f32", "f64"] as const
+).map((type) => ins(`${type}.const`, [type]));
+
+const NUMERIC_INSTRS: InstrValue[] = [
+ ...NO_IMM_NUMERIC_INSTRS.map((instr) => ins(instr, [])),
+ ...IMM_NUMERIC_INSTRS,
+];
// . vectors
export type VectorInstr = never;
+const VECTOR_INSTRS: InstrValue[] = [];
// . reference
@@ -146,12 +248,21 @@ export type ReferenceInstr =
| { kind: "ref.null"; imm: Reftype }
| { kind: "ref.is_null" }
| { kind: "ref.func"; imm: FuncIdx };
+const REFERENCE_INSTRS: InstrValue[] = [
+ ins("ref.null", ["refkind"]),
+ ins("ref.is_null", []),
+ ins("ref.func", ["i32"]),
+];
// . parametric
export type ParametricInstr =
| { kind: "drop" }
| { kind: "select"; type?: ValType[] };
+const PARAMETRIC_INSTRS: InstrValue[] = [
+ ins("drop", []),
+ ins("select", "select"),
+];
// . variable
@@ -164,6 +275,12 @@ export type VariableInstr =
kind: `global.${"get" | "set"}`;
imm: LocalIdx;
};
+const VARIABLE_INSTR: InstrValue[] = [
+ ...(["get", "set", "tee"] as const).map((kind) =>
+ ins(`local.${kind}`, ["i32"]),
+ ),
+ ...(["get", "set"] as const).map((kind) => ins(`global.${kind}`, ["i32"])),
+];
// . table
@@ -186,6 +303,14 @@ export type TableInstr =
kind: "elem.drop";
imm: ElemIdx;
};
+const TABLE_INSTRS: InstrValue[] = [
+ ...(["get", "set", "size", "grow", "fill"] as const).map((kind) =>
+ ins(`table.${kind}`, ["i32"]),
+ ),
+ ins("table.copy", ["i32", "i32"]),
+ ins("table.init", ["i32", "i32"]),
+ ins("elem.drop", ["i32"]),
+];
// . memory
@@ -197,6 +322,18 @@ export type MemArg = {
export type SimpleStoreKind = `${`${"i" | "f"}${BitWidth}` | "v128"}.${
| "load"
| "store"}`;
+const SIMPLE_STORES: SimpleStoreKind[] = [
+ "i32.load",
+ "i32.store",
+ "f32.load",
+ "f32.store",
+ "i64.load",
+ "i64.store",
+ "f64.load",
+ "f64.store",
+ "v128.load",
+ "v128.store",
+];
export type MemoryInstr =
| {
@@ -221,6 +358,35 @@ export type MemoryInstr =
imm: DataIdx;
}
| { kind: "data.drop"; imm: DataIdx };
+const MEMORY_INSTRS_WITH_MEMARG: MemoryInstr["kind"][] = [
+ ...SIMPLE_STORES,
+ "i32.load8_u",
+ "i32.load8_s",
+ "i32.load16_u",
+ "i32.load16_s",
+ "i64.load8_u",
+ "i64.load8_s",
+ "i64.load16_u",
+ "i64.load16_s",
+ //
+ "i64.load32_u",
+ "i64.load32_s",
+ //
+ "i32.store8",
+ "i32.store16",
+ "i64.store8",
+ "i64.store16",
+ "i64.store32",
+];
+const MEMORY_INSTRS: InstrValue[] = [
+ ...MEMORY_INSTRS_WITH_MEMARG.map((kind) => ins(kind, "memarg")),
+ ins("memory.size", []),
+ ins("memory.grow", []),
+ ins("memory.fill", []),
+ ins("memory.copy", []),
+ ins("memory.init", ["i32"]),
+ ins("data.drop", ["i32"]),
+];
// . control
@@ -278,6 +444,16 @@ export type Instr =
| MemoryInstr
| ControlInstr;
+export const INSTRS: InstrValue[] = [
+ ...NUMERIC_INSTRS,
+ ...VECTOR_INSTRS,
+ ...REFERENCE_INSTRS,
+ ...PARAMETRIC_INSTRS,
+ ...VARIABLE_INSTR,
+ ...TABLE_INSTRS,
+ ...MEMORY_INSTRS,
+];
+
export type Expr = Instr[];
// Modules
diff --git a/ui-tests/asm/drop.nil b/ui-tests/asm/drop.nil
new file mode 100644
index 0000000..6025e29
--- /dev/null
+++ b/ui-tests/asm/drop.nil
@@ -0,0 +1,10 @@
+//@check-pass
+
+function dropping(a: I32) =
+ ___asm(
+ __locals(),
+ "local.get 0",
+ "drop",
+ );
+
+function main() = ;
diff --git a/ui-tests/asm/instr_not_string.nil b/ui-tests/asm/instr_not_string.nil
new file mode 100644
index 0000000..2ac1287
--- /dev/null
+++ b/ui-tests/asm/instr_not_string.nil
@@ -0,0 +1,7 @@
+function a(a: I32) =
+ ___asm(
+ __locals(),
+ 0,
+ );
+
+function main() = ;
diff --git a/ui-tests/asm/instr_not_string.stderr b/ui-tests/asm/instr_not_string.stderr
new file mode 100644
index 0000000..1a146bd
--- /dev/null
+++ b/ui-tests/asm/instr_not_string.stderr
@@ -0,0 +1,4 @@
+error: inline assembly instruction must be string literal with instruction
+ --> $DIR/instr_not_string.nil:4
+4 | 0,
+ ^
diff --git a/ui-tests/asm/invalid_instr.nil b/ui-tests/asm/invalid_instr.nil
new file mode 100644
index 0000000..c6a1ed0
--- /dev/null
+++ b/ui-tests/asm/invalid_instr.nil
@@ -0,0 +1,9 @@
+//@check-pass
+
+function dropping(a: I32) =
+ ___asm(
+ __locals(),
+ "meow meow",
+ );
+
+function main() = ;
diff --git a/ui-tests/asm/invalid_instr.stderr b/ui-tests/asm/invalid_instr.stderr
new file mode 100644
index 0000000..d2345d7
--- /dev/null
+++ b/ui-tests/asm/invalid_instr.stderr
@@ -0,0 +1,4 @@
+error: unknown instruction: meow
+ --> $DIR/invalid_instr.nil:6
+6 | "meow meow",
+ ^^^^^^^^^^^
diff --git a/ui-tests/asm/missing_locals.nil b/ui-tests/asm/missing_locals.nil
new file mode 100644
index 0000000..20eef07
--- /dev/null
+++ b/ui-tests/asm/missing_locals.nil
@@ -0,0 +1,9 @@
+//@check-pass
+
+function dropping(a: I32) =
+ ___asm(
+ "local.get 0",
+ "drop",
+ );
+
+function main() = ;
diff --git a/ui-tests/asm/missing_locals.stderr b/ui-tests/asm/missing_locals.stderr
new file mode 100644
index 0000000..2d91691
--- /dev/null
+++ b/ui-tests/asm/missing_locals.stderr
@@ -0,0 +1,4 @@
+error: inline assembly must have __locals() as first argument
+ --> $DIR/missing_locals.nil:4
+4 | ___asm(
+ ^
diff --git a/ui-tests/asm/not_toplevel.nil b/ui-tests/asm/not_toplevel.nil
new file mode 100644
index 0000000..438304e
--- /dev/null
+++ b/ui-tests/asm/not_toplevel.nil
@@ -0,0 +1,6 @@
+function dropping(a: I32) = (
+ 1;
+ ___asm(__locals(), "drop");
+);
+
+function main() = ;
diff --git a/ui-tests/asm/not_toplevel.stderr b/ui-tests/asm/not_toplevel.stderr
new file mode 100644
index 0000000..d6f6444
--- /dev/null
+++ b/ui-tests/asm/not_toplevel.stderr
@@ -0,0 +1,16 @@
+error: `___asm` cannot be used as a value
+ --> $DIR/not_toplevel.nil:3
+3 | ___asm(__locals(), "drop");
+ ^^^^^^
+error: `__locals` cannot be used as a value
+ --> $DIR/not_toplevel.nil:3
+3 | ___asm(__locals(), "drop");
+ ^^^^^^^^
+error: expression of type is not callable
+ --> $DIR/not_toplevel.nil:3
+3 | ___asm(__locals(), "drop");
+ ^^^^^^^^
+error: expression of type is not callable
+ --> $DIR/not_toplevel.nil:3
+3 | ___asm(__locals(), "drop");
+ ^^^^^^
diff --git a/ui-tests/asm/wrong_imm.nil b/ui-tests/asm/wrong_imm.nil
new file mode 100644
index 0000000..7a4dc80
--- /dev/null
+++ b/ui-tests/asm/wrong_imm.nil
@@ -0,0 +1,17 @@
+//@check-pass
+
+function a(a: I32) =
+ ___asm(
+ __locals(),
+ "local.get 0 0",
+ "drop",
+ );
+
+function b(a: I32) =
+ ___asm(
+ __locals(),
+ "local.get",
+ "drop",
+ );
+
+function main() = ;
diff --git a/ui-tests/asm/wrong_imm.stderr b/ui-tests/asm/wrong_imm.stderr
new file mode 100644
index 0000000..c94c14f
--- /dev/null
+++ b/ui-tests/asm/wrong_imm.stderr
@@ -0,0 +1,8 @@
+error: mismatched immediate lengths, expected 1, got 2
+ --> $DIR/wrong_imm.nil:6
+6 | "local.get 0 0",
+ ^^^^^^^^^^^^^^^
+error: mismatched immediate lengths, expected 1, got 0
+ --> $DIR/wrong_imm.nil:13
+13 | "local.get",
+ ^^^^^^^^^^^
diff --git a/ui-tests/type/generics/generics_structs_in_args.stderr b/ui-tests/type/generics/generics_structs_in_args.stderr
new file mode 100644
index 0000000..f4499a9
--- /dev/null
+++ b/ui-tests/type/generics/generics_structs_in_args.stderr
@@ -0,0 +1,17 @@
+/home/nils/projects/riverdelta/target/ast.js:110
+ throw new Error(`substitution out of range, param index ${ty.idx} of param ${ty.name} out of range for length ${genericArgs.length}`);
+ ^
+
+Error: substitution out of range, param index 0 of param T out of range for length 0
+ at substituteTy (/home/nils/projects/riverdelta/target/ast.js:110:23)
+ at subst (/home/nils/projects/riverdelta/target/ast.js:106:27)
+ at Array.map ()
+ at substituteTy (/home/nils/projects/riverdelta/target/ast.js:125:45)
+ at typeOfItem (/home/nils/projects/riverdelta/target/typeck/item.js:193:33)
+ at Object.itemInner (/home/nils/projects/riverdelta/target/typeck/index.js:63:54)
+ at Object.item (/home/nils/projects/riverdelta/target/ast.js:146:34)
+ at /home/nils/projects/riverdelta/target/ast.js:164:55
+ at Array.map ()
+ at foldAst (/home/nils/projects/riverdelta/target/ast.js:164:34)
+
+Node.js v18.18.2