Skip to content

Commit

Permalink
add docs for assembly and fix global assembly parsing
Browse files Browse the repository at this point in the history
Previously, global assembly was parsed expecting it to have
the template syntax. However global assembly has no inputs,
outputs, or clobbers, and thus does not have template syntax.
This is now fixed.

This commit also adds a compile error for using volatile
on global assembly, since it is meaningless.

closes #1515
  • Loading branch information
andrewrk committed Mar 20, 2019
1 parent 3c7555c commit 15c316b
Show file tree
Hide file tree
Showing 10 changed files with 415 additions and 165 deletions.
28 changes: 21 additions & 7 deletions doc/docgen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ const Code = struct {
is_inline: bool,
mode: builtin.Mode,
link_objects: []const []const u8,
target_windows: bool,
target_str: ?[]const u8,
link_libc: bool,

const Id = union(enum) {
Expand Down Expand Up @@ -491,7 +491,7 @@ fn genToc(allocator: *mem.Allocator, tokenizer: *Tokenizer) !Toc {
var mode = builtin.Mode.Debug;
var link_objects = std.ArrayList([]const u8).init(allocator);
defer link_objects.deinit();
var target_windows = false;
var target_str: ?[]const u8 = null;
var link_libc = false;

const source_token = while (true) {
Expand All @@ -506,7 +506,9 @@ fn genToc(allocator: *mem.Allocator, tokenizer: *Tokenizer) !Toc {
const obj_tok = try eatToken(tokenizer, Token.Id.TagContent);
try link_objects.append(tokenizer.buffer[obj_tok.start..obj_tok.end]);
} else if (mem.eql(u8, end_tag_name, "target_windows")) {
target_windows = true;
target_str = "x86_64-windows";
} else if (mem.eql(u8, end_tag_name, "target_linux_x86_64")) {
target_str = "x86_64-linux";
} else if (mem.eql(u8, end_tag_name, "link_libc")) {
link_libc = true;
} else if (mem.eql(u8, end_tag_name, "code_end")) {
Expand All @@ -526,7 +528,7 @@ fn genToc(allocator: *mem.Allocator, tokenizer: *Tokenizer) !Toc {
.is_inline = is_inline,
.mode = mode,
.link_objects = link_objects.toOwnedSlice(),
.target_windows = target_windows,
.target_str = target_str,
.link_libc = link_libc,
},
});
Expand Down Expand Up @@ -998,7 +1000,7 @@ fn genHtml(allocator: *mem.Allocator, tokenizer: *Tokenizer, toc: *Toc, out: var
try io.writeFile(tmp_source_file_name, trimmed_raw_source);

switch (code.id) {
Code.Id.Exe => |expected_outcome| {
Code.Id.Exe => |expected_outcome| code_block: {
const name_plus_bin_ext = try std.fmt.allocPrint(allocator, "{}{}", code.name, exe_ext);
const tmp_bin_file_name = try os.path.join(
allocator,
Expand Down Expand Up @@ -1046,8 +1048,20 @@ fn genHtml(allocator: *mem.Allocator, tokenizer: *Tokenizer, toc: *Toc, out: var
try build_args.append("c");
try out.print(" --library c");
}
if (code.target_str) |triple| {
try build_args.appendSlice([][]const u8{ "-target", triple });
}
_ = exec(allocator, &env_map, build_args.toSliceConst()) catch return parseError(tokenizer, code.source_token, "example failed to compile");

if (code.target_str) |triple| {
if (mem.startsWith(u8, triple, "x86_64-linux") and
(builtin.os != builtin.Os.linux or builtin.arch != builtin.Arch.x86_64))
{
// skip execution
break :code_block;
}
}

const run_args = [][]const u8{tmp_bin_file_name};

const result = if (expected_outcome == ExpectedOutcome.Fail) blk: {
Expand Down Expand Up @@ -1105,8 +1119,8 @@ fn genHtml(allocator: *mem.Allocator, tokenizer: *Tokenizer, toc: *Toc, out: var
try out.print(" --release-small");
},
}
if (code.target_windows) {
try test_args.appendSlice([][]const u8{ "-target", "x86_64-windows" });
if (code.target_str) |triple| {
try test_args.appendSlice([][]const u8{ "-target", triple });
}
const result = exec(allocator, &env_map, test_args.toSliceConst()) catch return parseError(tokenizer, code.source_token, "test failed");
const escaped_stderr = try escapeHtml(allocator, result.stderr);
Expand Down
193 changes: 189 additions & 4 deletions doc/langref.html.in
Original file line number Diff line number Diff line change
Expand Up @@ -5396,11 +5396,196 @@ pub fn main() void {
{#see_also|inline while|inline for#}
{#header_close#}
{#header_open|Assembly#}
<p>TODO: example of inline assembly</p>
<p>TODO: example of module level assembly</p>
<p>TODO: example of using inline assembly return value</p>
<p>TODO: example of using inline assembly assigning values to variables</p>
<p>
For some use cases, it may be necessary to directly control the machine code generated
by Zig programs, rather than relying on Zig's code generation. For these cases, one
can use inline assembly. Here is an example of implementing Hello, World on x86_64 Linux
using inline assembly:
</p>
{#code_begin|exe#}
{#target_linux_x86_64#}
pub fn main() noreturn {
const msg = "hello world\n";
_ = syscall3(SYS_write, STDOUT_FILENO, @ptrToInt(&msg), msg.len);
_ = syscall1(SYS_exit, 0);
unreachable;
}

pub const SYS_write = 1;
pub const SYS_exit = 60;

pub const STDOUT_FILENO = 1;

pub fn syscall1(number: usize, arg1: usize) usize {
return asm volatile ("syscall"
: [ret] "={rax}" (-> usize)
: [number] "{rax}" (number),
[arg1] "{rdi}" (arg1)
: "rcx", "r11"
);
}

pub fn syscall3(number: usize, arg1: usize, arg2: usize, arg3: usize) usize {
return asm volatile ("syscall"
: [ret] "={rax}" (-> usize)
: [number] "{rax}" (number),
[arg1] "{rdi}" (arg1),
[arg2] "{rsi}" (arg2),
[arg3] "{rdx}" (arg3)
: "rcx", "r11"
);
}
{#code_end#}
<p>
Dissecting the syntax:
</p>
<pre>{#syntax#}// Inline assembly is an expression which returns a value.
// the `asm` keyword begins the expression.
_ = asm
// `volatile` is an optional modifier that tells Zig this
// inline assembly expression has side-effects. Without

This comment has been minimized.

Copy link
@daurnimator

daurnimator Mar 20, 2019

Contributor

Is it? I thought volatile on assembly tells the compiler to use it as is, and to not allow the optimizer to change it?

This comment has been minimized.

Copy link
@andrewrk

andrewrk Mar 20, 2019

Author Member

The docs here are correct. Optimizer is not allowed to modify the inline assembly but it is allowed to discard it if non-volatile (and the result ends up being unused).

// `volatile`, Zig is allowed to delete the inline assembly
// code if the result is unused.
volatile (
// Next is a comptime string which is the assembly code.
// Inside this string one may use `%[ret]`, `%[number]`,
// or `%[arg1]` where a register is expected, to specify
// the register that Zig uses for the argument or return value,
// if the register constraint strings are used. However in
// the below code, this is not used. A literal `%` can be
// obtained by escaping it with a double percent: `%%`.
// Often multiline string syntax comes in handy here.
\\syscall
// Next is the output. It is possible in the future Zig will
// support multiple outputs, depending on how
// https://github.com/ziglang/zig/issues/215 is resolved.
// It is allowed for there to be no outputs, in which case
// this colon would be directly followed by the colon for the inputs.
:
// This specifies the name to be used in `%[ret]` syntax in
// the above assembly string. This example does not use it,
// but the syntax is mandatory.
[ret]
// Next is the output constraint string. This feature is still
// considered unstable in Zig, and so LLVM/GCC documentation
// must be used to understand the semantics.
// http://releases.llvm.org/8.0.0/docs/LangRef.html#inline-asm-constraint-string
// https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html
// In this example, the constraint string means "the result value of
// this inline assembly instruction is whatever is in $rax".
"={rax}"
// Next is either a value binding, or `->` and then a type. The
// type is the result type of the inline assembly expression.
// If it is a value binding, then `%[ret]` syntax would be used
// to refer to the register bound to the value.
(-> usize)
// Next is the list of inputs.
// The constraint for these inputs means, "when the assembly code is
// executed, $rax shall have the value of `number` and $rdi shall have
// the value of `arg1`". Any number of input parameters is allowed,
// including none.
: [number] "{rax}" (number),
[arg1] "{rdi}" (arg1)
// Next is the list of clobbers. These declare a set of registers whose
// values will not be preserved by the execution of this assembly code.
// These do not include output or input registers. The special clobber
// value of "memory" means that the assembly writes to arbitrary undeclared
// memory locations - not only the memory pointed to by a declared indirect
// output. In this example we list $rcx and $r11 because it is known the
// kernel syscall does not preserve these registers.
: "rcx", "r11"
);{#endsyntax#}</pre>
<p>
For i386 and x86_64 targets, the syntax is AT&amp;T syntax, rather than the more

This comment has been minimized.

Copy link
@daurnimator

daurnimator Mar 20, 2019

Contributor

Do you envision a parameter/annotation to swap between syntaxes in future? How might this look?

This comment has been minimized.

Copy link
@andrewrk

andrewrk Mar 20, 2019

Author Member

I created #2081 for this discussion - want to ask there?

popular Intel syntax. This is due to technical constraints; assembly parsing is
provided by LLVM and its support for Intel syntax is buggy and not well tested.
</p>
<p>
Some day Zig may have its own assembler. This would allow it to integrate more seamlessly
into the language, as well as be compatible with the popular NASM syntax. This documentation
section will be updated before 1.0.0 is released, with a conclusive statement about the status
of AT&amp;T vs Intel/NASM syntax.
</p>
{#header_open|Output Constraints#}
<p>
Output constraints are still considered to be unstable in Zig, and
so
<a href="http://releases.llvm.org/8.0.0/docs/LangRef.html#inline-asm-constraint-string">LLVM documentation</a>
and
<a href="https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html">GCC documentation</a>
must be used to understand the semantics.
</p>
<p>
Note that some breaking changes to output constraints are planned with
<a href="https://github.com/ziglang/zig/issues/215">issue #215</a>.
</p>
{#header_close#}

{#header_open|Input Constraints#}
<p>
Input constraints are still considered to be unstable in Zig, and
so
<a href="http://releases.llvm.org/8.0.0/docs/LangRef.html#inline-asm-constraint-string">LLVM documentation</a>
and
<a href="https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html">GCC documentation</a>
must be used to understand the semantics.
</p>
<p>
Note that some breaking changes to input constraints are planned with
<a href="https://github.com/ziglang/zig/issues/215">issue #215</a>.
</p>
{#header_close#}

{#header_open|Clobbers#}
<p>
Clobbers are the set of registers whose values will not be preserved by the execution of
the assembly code. These do not include output or input registers. The special clobber
value of {#syntax#}"memory"{#endsyntax#} means that the assembly causes writes to
arbitrary undeclared memory locations - not only the memory pointed to by a declared
indirect output.
</p>
<p>
Failure to declare the full set of clobbers for a given inline assembly
expression is unchecked {#link|Undefined Behavior#}.
</p>
{#header_close#}

{#header_open|Global Assembly#}
<p>
When an assembly expression occurs in a top level {#link|comptime#} block, this is
<strong>global assembly</strong>.
</p>
<p>
This kind of assembly has different rules than inline assembly. First, {#syntax#}volatile{#endsyntax#}
is not valid because all global assembly is unconditionally included.
Second, there are no inputs, outputs, or clobbers. All global assembly is concatenated
verbatim into one long string and assembled together. There are no template substitution rules regarding
<code>%</code> as there are in inline assembly expressions.
</p>
{#code_begin|test|global-asm#}
{#target_linux_x86_64#}
const std = @import("std");
const assert = std.debug.assert;

comptime {
asm (
\\.global my_func;
\\.type my_func, @function;
\\my_func:
\\ lea (%rdi,%rsi,1),%eax
\\ retq
);
}

extern fn my_func(a: i32, b: i32) i32;

test "global assembly" {
assert(my_func(12, 34) == 46);
}
{#code_end#}
{#header_close#}
{#header_close#}

{#header_open|Atomics#}
<p>TODO: @fence()</p>
<p>TODO: @atomic rmw</p>
Expand Down
16 changes: 12 additions & 4 deletions src/all_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -800,9 +800,8 @@ struct AsmToken {
};

struct AstNodeAsmExpr {
bool is_volatile;
Buf *asm_template;
ZigList<AsmToken> token_list;
Token *volatile_token;
Token *asm_template;
ZigList<AsmOutput*> output_list;
ZigList<AsmInput*> input_list;
ZigList<Buf*> clobber_list;
Expand Down Expand Up @@ -2169,6 +2168,7 @@ enum IrInstructionId {
IrInstructionIdArrayType,
IrInstructionIdPromiseType,
IrInstructionIdSliceType,
IrInstructionIdGlobalAsm,
IrInstructionIdAsm,
IrInstructionIdSizeOf,
IrInstructionIdTestNonNull,
Expand Down Expand Up @@ -2677,10 +2677,18 @@ struct IrInstructionSliceType {
bool allow_zero;
};

struct IrInstructionGlobalAsm {
IrInstruction base;

Buf *asm_code;
};

struct IrInstructionAsm {
IrInstruction base;

// Most information on inline assembly comes from the source node.
Buf *asm_template;
AsmToken *token_list;
size_t token_list_len;
IrInstruction **input_list;
IrInstruction **output_types;
ZigVar **output_vars;
Expand Down
4 changes: 2 additions & 2 deletions src/ast_render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -862,8 +862,8 @@ static void render_node_extra(AstRender *ar, AstNode *node, bool grouped) {
case NodeTypeAsmExpr:
{
AstNodeAsmExpr *asm_expr = &node->data.asm_expr;
const char *volatile_str = asm_expr->is_volatile ? " volatile" : "";
fprintf(ar->f, "asm%s (\"%s\"\n", volatile_str, buf_ptr(asm_expr->asm_template));
const char *volatile_str = (asm_expr->volatile_token != nullptr) ? " volatile" : "";
fprintf(ar->f, "asm%s (\"%s\"\n", volatile_str, buf_ptr(&asm_expr->asm_template->data.str_lit.str));
print_indent(ar);
fprintf(ar->f, ": ");
for (size_t i = 0; i < asm_expr->output_list.length; i += 1) {
Expand Down
15 changes: 8 additions & 7 deletions src/codegen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3793,8 +3793,8 @@ static LLVMValueRef ir_render_union_field_ptr(CodeGen *g, IrExecutable *executab
return bitcasted_union_field_ptr;
}

static size_t find_asm_index(CodeGen *g, AstNode *node, AsmToken *tok) {
const char *ptr = buf_ptr(node->data.asm_expr.asm_template) + tok->start + 2;
static size_t find_asm_index(CodeGen *g, AstNode *node, AsmToken *tok, Buf *src_template) {
const char *ptr = buf_ptr(src_template) + tok->start + 2;
size_t len = tok->end - tok->start - 2;
size_t result = 0;
for (size_t i = 0; i < node->data.asm_expr.output_list.length; i += 1, result += 1) {
Expand All @@ -3817,13 +3817,13 @@ static LLVMValueRef ir_render_asm(CodeGen *g, IrExecutable *executable, IrInstru
assert(asm_node->type == NodeTypeAsmExpr);
AstNodeAsmExpr *asm_expr = &asm_node->data.asm_expr;

Buf *src_template = asm_expr->asm_template;
Buf *src_template = instruction->asm_template;

Buf llvm_template = BUF_INIT;
buf_resize(&llvm_template, 0);

for (size_t token_i = 0; token_i < asm_expr->token_list.length; token_i += 1) {
AsmToken *asm_token = &asm_expr->token_list.at(token_i);
for (size_t token_i = 0; token_i < instruction->token_list_len; token_i += 1) {
AsmToken *asm_token = &instruction->token_list[token_i];
switch (asm_token->id) {
case AsmTokenIdTemplate:
for (size_t offset = asm_token->start; offset < asm_token->end; offset += 1) {
Expand All @@ -3840,7 +3840,7 @@ static LLVMValueRef ir_render_asm(CodeGen *g, IrExecutable *executable, IrInstru
break;
case AsmTokenIdVar:
{
size_t index = find_asm_index(g, asm_node, asm_token);
size_t index = find_asm_index(g, asm_node, asm_token, src_template);
assert(index < SIZE_MAX);
buf_appendf(&llvm_template, "$%" ZIG_PRI_usize "", index);
break;
Expand Down Expand Up @@ -3937,7 +3937,7 @@ static LLVMValueRef ir_render_asm(CodeGen *g, IrExecutable *executable, IrInstru
}
LLVMTypeRef function_type = LLVMFunctionType(ret_type, param_types, (unsigned)input_and_output_count, false);

bool is_volatile = asm_expr->is_volatile || (asm_expr->output_list.length == 0);
bool is_volatile = instruction->has_side_effects || (asm_expr->output_list.length == 0);
LLVMValueRef asm_fn = LLVMGetInlineAsm(function_type, buf_ptr(&llvm_template), buf_len(&llvm_template),
buf_ptr(&constraint_buf), buf_len(&constraint_buf), is_volatile, false, LLVMInlineAsmDialectATT);

Expand Down Expand Up @@ -5480,6 +5480,7 @@ static LLVMValueRef ir_render_instruction(CodeGen *g, IrExecutable *executable,
case IrInstructionIdCmpxchgSrc:
case IrInstructionIdLoadPtr:
case IrInstructionIdBitCast:
case IrInstructionIdGlobalAsm:
zig_unreachable();

case IrInstructionIdDeclVarGen:
Expand Down
Loading

0 comments on commit 15c316b

Please sign in to comment.