mirror of
https://github.com/odin-lang/Odin.git
synced 2026-07-17 13:11:07 +00:00
Begin work on the module encoder
This commit is contained in:
@@ -21,10 +21,9 @@ import "core:math/bits"
|
||||
// 5-byte LEB placeholders so the patched value always fits.
|
||||
//
|
||||
// The reusable core is `encode_ops` (an operation stream = a WASM `expr`). The
|
||||
// Module verb `encode` drives it over each function's body blocks. Byte-level
|
||||
// container framing (the type/function/code section wrappers) is a separate,
|
||||
// symmetric concern -- the sibling container reader lives in the WASM `module`
|
||||
// parsing path -- and is not part of this instruction-stream codec.
|
||||
// container `encode` verb (write.odin) wraps those bodies in the CODE-section
|
||||
// framing and emits the surrounding sections, producing a whole `.wasm` file;
|
||||
// this file owns only the instruction-stream level.
|
||||
|
||||
MAX_OPCODE_SIZE :: 3 // prefix byte + two-byte unsigned-LEB sub-opcode (SIMD reaches 0x113)
|
||||
|
||||
@@ -40,26 +39,6 @@ encode_max_relocation_count :: #force_inline proc "contextless" (n: int) -> int
|
||||
return n
|
||||
}
|
||||
|
||||
// encode: serialize the module's function bodies into `code`, in order. Returns
|
||||
// the total byte count written. (A Module built as one function / one block
|
||||
// reproduces the old flat `encode([]Instruction)` behavior exactly.)
|
||||
encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) {
|
||||
// TODO(bill): This just writes the functions and nothing to do with the module section stuff
|
||||
errors_start := u32(len(errors))
|
||||
op_index := u16(0)
|
||||
for fn in m.functions {
|
||||
for blk in fn.blocks {
|
||||
for &op in blk.ops {
|
||||
n := encode_operation(&op, byte_count, op_index, code, relocs, errors) or_return
|
||||
byte_count += n
|
||||
op_index += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
ok = u32(len(errors)) == errors_start
|
||||
return
|
||||
}
|
||||
|
||||
// encode_ops: the reusable instruction-stream encoder (a WASM `expr`).
|
||||
encode_ops :: proc(ops: []Operation, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) {
|
||||
errors_start := u32(len(errors))
|
||||
|
||||
@@ -160,3 +160,26 @@ read_u64_block :: #force_inline proc "contextless" (data: []u8, offset: ^u32) ->
|
||||
offset^ += 8
|
||||
return v, true
|
||||
}
|
||||
|
||||
|
||||
// Unsigned LEB128, appended to a [dynamic]u8
|
||||
append_uleb :: proc(b: ^[dynamic]u8, value: u64) {
|
||||
v := value
|
||||
for {
|
||||
bb := u8(v & 0x7F)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
bb |= 0x80
|
||||
}
|
||||
append(b, bb)
|
||||
if v == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
append_uleb_name :: proc(b: ^[dynamic]u8, s: string) {
|
||||
append_uleb(b, u64(len(s)))
|
||||
append(b, s)
|
||||
}
|
||||
|
||||
317
core/rexcode/ir/wasm/write.odin
Normal file
317
core/rexcode/ir/wasm/write.odin
Normal file
@@ -0,0 +1,317 @@
|
||||
// rexcode · Brendan Punsky (dotbmp@github), original author
|
||||
// Ginger Bill (gingerBill@github)
|
||||
|
||||
package rexcode_wasm
|
||||
|
||||
import "base:runtime"
|
||||
import "core:slice"
|
||||
|
||||
// =============================================================================
|
||||
// SECTION: Container encode (ir Module -> a whole .wasm binary module)
|
||||
// =============================================================================
|
||||
//
|
||||
// The mirror of parse.odin. `encode_ops` (encoder.odin) serializes one WASM
|
||||
// `expr` -- a single function's instruction stream. This file is the outer
|
||||
// layer parse.odin reads: it wraps those bodies in the CODE section framing and
|
||||
// emits the surrounding sections so `encode` produces a complete `.wasm` file
|
||||
// (the 8-byte header plus the length-prefixed sections), not a bare run of
|
||||
// concatenated bodies.
|
||||
//
|
||||
// Two shapes of module flow through here, dispatched on whether the module
|
||||
// carries the binary framing decode preserved (`sections` + `data`):
|
||||
//
|
||||
// * A decoded module (round-trip / edit-a-body): every section is re-emitted
|
||||
// verbatim from `data` in its original order, EXCEPT CODE, which is
|
||||
// regenerated from the ir Operations (so edits to a function body show up).
|
||||
// A decode -> encode with no edits reproduces the input byte-for-byte; the
|
||||
// sections parse.odin does not model structurally (table / memory / global
|
||||
// / element / data, non-func imports, custom sections) are never lost.
|
||||
//
|
||||
// * A from-scratch module (built via make_module + the builders): there is no
|
||||
// `data` to copy, so the sections the ir core *does* model are synthesized
|
||||
// in the canonical WASM order -- TYPE, IMPORT (func), FUNCTION, EXPORT,
|
||||
// START, CODE. Sections with no structural slot (table/memory/global/...)
|
||||
// cannot be synthesized and are simply absent.
|
||||
//
|
||||
// Emitted relocations (for symbolic index refs inside bodies, e.g. op_label)
|
||||
// are offset relative to the produced `code` buffer, and carried out through
|
||||
// `relocs` for a linker; body errors surface through `errors` as usual.
|
||||
|
||||
// encode: serialize the module `m` into `code`, producing a complete `.wasm`
|
||||
// binary. Returns the number of bytes written and whether it fully succeeded.
|
||||
// Size `code` with `encode_size(m)` (or any larger buffer).
|
||||
encode :: proc(m: Module, code: []u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (byte_count: u32, ok: bool) {
|
||||
errors_start := u32(len(errors))
|
||||
|
||||
out := build_module(m, relocs, errors)
|
||||
defer delete(out)
|
||||
|
||||
if len(out) > len(code) {
|
||||
append(errors, Error{location = 0, code = .BUFFER_OVERFLOW})
|
||||
return 0, false
|
||||
}
|
||||
copy(code, out[:])
|
||||
byte_count = u32(len(out))
|
||||
ok = u32(len(errors)) == errors_start
|
||||
return
|
||||
}
|
||||
|
||||
// encode_size: the exact byte length `encode(m, ...)` will produce (a dry run,
|
||||
// so callers can size the output buffer precisely). Uses the temp allocator.
|
||||
@(require_results)
|
||||
encode_size :: proc(m: Module) -> u32 {
|
||||
throw_r: [dynamic]Relocation; throw_r.allocator = runtime.nil_allocator()
|
||||
throw_e: [dynamic]Error; throw_e.allocator = runtime.nil_allocator()
|
||||
out := build_module(m, &throw_r, &throw_e)
|
||||
defer delete(out)
|
||||
return u32(len(out))
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Module builder (into a temp-allocated byte buffer)
|
||||
// =============================================================================
|
||||
|
||||
@(private, require_results)
|
||||
build_module :: proc(m: Module, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) -> (out: [dynamic]u8) {
|
||||
append_u32le :: #force_inline proc(b: ^[dynamic]u8, v: u32) {
|
||||
append(b, u8(v), u8(v >> 8), u8(v >> 16), u8(v >> 24))
|
||||
}
|
||||
|
||||
out.allocator = context.temp_allocator
|
||||
|
||||
// header: `\0asm` magic + version.
|
||||
append_u32le(&out, WASM_MAGIC)
|
||||
append_u32le(&out, m.version if m.version != 0 else WASM_VERSION)
|
||||
|
||||
if len(m.sections) > 0 && m.data != nil {
|
||||
emit_passthrough(m, &out, relocs, errors)
|
||||
} else {
|
||||
emit_synth(m, &out, relocs, errors)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Decoded module: re-emit each section verbatim in file order, regenerating
|
||||
// only CODE from the ir Operations.
|
||||
@(private)
|
||||
emit_passthrough :: proc(m: Module, out: ^[dynamic]u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) {
|
||||
for sec in m.sections {
|
||||
if sec.id == .CODE {
|
||||
emit_code_section(m, out, relocs, errors)
|
||||
} else {
|
||||
append(out, u8(sec.id))
|
||||
append_uleb(out, u64(sec.size))
|
||||
append(out, ..m.data[sec.offset:][:sec.size])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// From-scratch module: synthesize the modeled sections in canonical order.
|
||||
@(private)
|
||||
emit_synth :: proc(m: Module, out: ^[dynamic]u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) {
|
||||
// id byte ++ uleb(len contents) ++ contents
|
||||
emit_section :: proc(out: ^[dynamic]u8, id: Section_Id, contents: []u8) {
|
||||
append(out, u8(id))
|
||||
append_uleb(out, u64(len(contents)))
|
||||
append(out, ..contents)
|
||||
}
|
||||
|
||||
|
||||
tmp := context.temp_allocator
|
||||
|
||||
// TYPE
|
||||
if len(m.func_types) > 0 {
|
||||
c: [dynamic]u8; c.allocator = tmp
|
||||
append_uleb(&c, u64(len(m.func_types)))
|
||||
for ft in m.func_types {
|
||||
append(&c, 0x60) // functype form
|
||||
|
||||
#assert(size_of(ft.params[0]) == 1)
|
||||
#assert(size_of(ft.results[0]) == 1)
|
||||
|
||||
append_uleb(&c, u64(len(ft.params)))
|
||||
append(&c, ..slice.to_bytes(ft.params[:]))
|
||||
|
||||
append_uleb(&c, u64(len(ft.results)))
|
||||
append(&c, ..slice.to_bytes(ft.results[:]))
|
||||
}
|
||||
emit_section(out, .TYPE, c[:])
|
||||
}
|
||||
|
||||
// IMPORT (only the func kind is structurally modeled)
|
||||
if len(m.imports) > 0 {
|
||||
c: [dynamic]u8; c.allocator = tmp
|
||||
append_uleb(&c, u64(len(m.imports)))
|
||||
for imp in m.imports {
|
||||
append_uleb_name(&c, imp.module_name)
|
||||
append_uleb_name(&c, imp.field_name)
|
||||
append(&c, u8(imp.kind))
|
||||
#partial switch imp.kind {
|
||||
case .FUNC:
|
||||
append_uleb(&c, u64(imp.index)) // typeidx
|
||||
case:
|
||||
// table/memory/global descriptors are not modeled from scratch.
|
||||
append(errors, Error{location = 0, code = .UNSUPPORTED_FEATURE})
|
||||
}
|
||||
}
|
||||
emit_section(out, .IMPORT, c[:])
|
||||
}
|
||||
|
||||
// FUNCTION (typeidx per *defined* function -- those with a body)
|
||||
if ndef := defined_function_count(m); ndef > 0 {
|
||||
c: [dynamic]u8; c.allocator = tmp
|
||||
append_uleb(&c, u64(ndef))
|
||||
for f in m.functions {
|
||||
if len(f.blocks) == 0 {
|
||||
continue // imported: no FUNCTION entry
|
||||
}
|
||||
append_uleb(&c, u64(u32(f.signature)))
|
||||
}
|
||||
emit_section(out, .FUNCTION, c[:])
|
||||
}
|
||||
|
||||
// EXPORT
|
||||
if len(m.exports) > 0 {
|
||||
c: [dynamic]u8; c.allocator = tmp
|
||||
append_uleb(&c, u64(len(m.exports)))
|
||||
for e in m.exports {
|
||||
append_uleb_name(&c, e.name)
|
||||
append(&c, u8(e.kind))
|
||||
append_uleb(&c, u64(e.index))
|
||||
}
|
||||
emit_section(out, .EXPORT, c[:])
|
||||
}
|
||||
|
||||
// START
|
||||
if m.start >= 0 {
|
||||
c: [dynamic]u8; c.allocator = tmp
|
||||
append_uleb(&c, u64(u32(m.start)))
|
||||
emit_section(out, .START, c[:])
|
||||
}
|
||||
|
||||
// CODE
|
||||
emit_code_section(m, out, relocs, errors)
|
||||
|
||||
// the remaining sections
|
||||
for sec in m.sections {
|
||||
switch sec.id {
|
||||
case .CUSTOM, .TABLE, .MEMORY, .GLOBAL, .ELEMENT, .DATA, .DATA_COUNT:
|
||||
emit_section(out, sec.id, m.data[sec.offset:][:sec.size])
|
||||
case .TYPE, .IMPORT, .FUNCTION, .EXPORT, .START, .CODE:
|
||||
// handled above
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// CODE section (shared by both modes -- always regenerated from the ir ops)
|
||||
// =============================================================================
|
||||
|
||||
@(private)
|
||||
emit_code_section :: proc(m: Module, out: ^[dynamic]u8, relocs: ^[dynamic]Relocation, errors: ^[dynamic]Error) {
|
||||
tmp := context.temp_allocator
|
||||
|
||||
ndef := defined_function_count(m)
|
||||
if ndef == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
contents: [dynamic]u8; contents.allocator = tmp
|
||||
append_uleb(&contents, u64(ndef))
|
||||
|
||||
// Body relocations, collected with offsets relative to the CODE contents;
|
||||
// rebased to buffer-relative once the section is placed.
|
||||
code_relocs: [dynamic]Relocation; code_relocs.allocator = tmp
|
||||
|
||||
for f, fi in m.functions {
|
||||
if len(f.blocks) == 0 {
|
||||
continue // imported function: no body
|
||||
}
|
||||
|
||||
// A WASM function body is one `expr`. Concatenate all blocks' ops (the
|
||||
// decoder produces one block; a hand-built function may use several).
|
||||
all_ops := f.blocks[0].ops
|
||||
if len(f.blocks) > 1 {
|
||||
acc: [dynamic]Operation; acc.allocator = tmp
|
||||
for blk in f.blocks {
|
||||
append(&acc, ..blk.ops)
|
||||
}
|
||||
all_ops = acc[:]
|
||||
}
|
||||
|
||||
// body = locals vector ++ expr bytes
|
||||
body: [dynamic]u8; body.allocator = tmp
|
||||
locals: []Value_Type
|
||||
if fi < len(m.function_locals) {
|
||||
locals = m.function_locals[fi]
|
||||
}
|
||||
emit_locals(&body, locals)
|
||||
expr_off_in_body := u32(len(body))
|
||||
|
||||
scratch := make([]u8, encode_max_code_size(len(all_ops)) + 16, tmp)
|
||||
body_relocs: [dynamic]Relocation; body_relocs.allocator = tmp
|
||||
n, _ := encode_ops(all_ops, scratch, &body_relocs, errors)
|
||||
append(&body, ..scratch[:n])
|
||||
|
||||
// entry = uleb(body_len) ++ body
|
||||
append_uleb(&contents, u64(len(body)))
|
||||
body_start_in_contents := u32(len(contents))
|
||||
append(&contents, ..body[:])
|
||||
|
||||
expr_off_in_contents := body_start_in_contents + expr_off_in_body
|
||||
for rr in body_relocs {
|
||||
r2 := rr
|
||||
r2.offset += expr_off_in_contents
|
||||
append(&code_relocs, r2)
|
||||
}
|
||||
}
|
||||
|
||||
// Emit the framed section; learn where its contents landed in the buffer.
|
||||
append(out, u8(Section_Id.CODE))
|
||||
append_uleb(out, u64(len(contents)))
|
||||
code_contents_file_off := u32(len(out))
|
||||
append(out, ..contents[:])
|
||||
|
||||
for rr in code_relocs {
|
||||
r2 := rr
|
||||
r2.offset += code_contents_file_off
|
||||
append(relocs, r2)
|
||||
}
|
||||
}
|
||||
|
||||
// A function's declared locals, re-compressed into `count x type` groups (runs
|
||||
// of the same value type), exactly as the code section expects.
|
||||
@(private)
|
||||
emit_locals :: proc(body: ^[dynamic]u8, locals: []Value_Type) {
|
||||
group_count := u32(0)
|
||||
for i := 0; i < len(locals); /**/ {
|
||||
j := i+1
|
||||
for j < len(locals) && locals[j] == locals[i] {
|
||||
j += 1
|
||||
}
|
||||
group_count += 1
|
||||
i = j
|
||||
}
|
||||
append_uleb(body, u64(group_count))
|
||||
for i := 0; i < len(locals); /**/ {
|
||||
j := i+1
|
||||
for j < len(locals) && locals[j] == locals[i] {
|
||||
j += 1
|
||||
}
|
||||
append_uleb(body, u64(j - i))
|
||||
append(body, u8(locals[i]))
|
||||
i = j
|
||||
}
|
||||
}
|
||||
|
||||
@(private, require_results)
|
||||
defined_function_count :: proc "contextless" (m: Module) -> int {
|
||||
n := 0
|
||||
for f in m.functions {
|
||||
if len(f.blocks) > 0 {
|
||||
n += 1
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
Reference in New Issue
Block a user