Current section
Files
Jump to
Current section
Files
lib/zorb/capsule/assembler.ex
defmodule Zorb.Capsule.Assembler do # credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity # credo:disable-for-this-file Credo.Check.Refactor.Nesting # credo:disable-for-this-file Credo.Check.Refactor.LongQuoteBlocks @moduledoc false require Logger @interpreter_source_path Path.expand("../interpreter.ex", __DIR__) @external_resource @interpreter_source_path @interpreter_ast Sourceror.parse_string!(File.read!(@interpreter_source_path)) defp fat_ast, do: unquote(Macro.escape(@interpreter_ast)) def assemble(story_data, module_name) do <<version::8, _::binary>> = story_data # 1. Read fat AST and prune version branches ast = fat_ast() |> prune_version_branches(version) # 2. Prepare Data {gb, smb, db, ab, otb} = extract_header_fields(story_data) {hash_table, mask} = generate_dictionary_hash_table(story_data, db, version) pruned_story = prune_story_data(story_data, dictionary_base: db) {pas, oes, po, soj, co, pto} = calculate_version_constants(version) {ro, so} = calculate_offsets(version, story_data) # Unicode table only needed for V5+ unicode = case version >= 5 do true -> generate_unicode_binary() false -> <<>> end metadata = Zorb.Inspector.analyze(story_data) metadata_bin = <<metadata.version::8, metadata.serial::binary-size(6), 0::8, metadata.command_prefix::8, 0::56>> <> String.pad_trailing(String.slice(metadata.chat_prefix, 0, 63), 64, <<0>>) <> String.pad_trailing(String.slice(metadata.channel_prefix, 0, 63), 64, <<0>>) # Spec 3.5.3: Alphabets. Note: zchars 0-5 are special, table starts at zchar 6. a0 = Enum.to_list(?a..?z) a1 = Enum.to_list(?A..?Z) # V1 A2: Spec 3.5.4 a2_v1 = [ ?\s, ?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?., ?,, ?!, ??, ?_, ?#, ?', ?\", ?/, ?\\, ?<, ?-, ?:, ?(, ?) ] # V2+ A2: Spec 3.5.3 a2_v2 = [ 0, 13, ?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?., ?,, ?!, ??, ?_, ?#, ?', ?\", ?/, ?\\, ?-, ?:, ?(, ?) ] # Prune unused alphabet based on version alphabets = case version do 1 -> a0 ++ a1 ++ a2_v1 ++ List.duplicate(0, 26) _ -> a0 ++ a1 ++ List.duplicate(0, 26) ++ a2_v2 end if length(alphabets) != 104, do: raise("Wrong alphabet size: #{length(alphabets)}") # Create a payload of baked data to be loaded during module compilation chunk_size = 4096 story_chunks = for offset <- 0..(byte_size(pruned_story) - 1) |> Enum.take_every(chunk_size) do len = min(chunk_size, byte_size(pruned_story) - offset) {offset, :binary.bin_to_list(binary_part(pruned_story, offset, len))} end payload = %{ story_chunks: story_chunks, unicode: :binary.bin_to_list(unicode), hash: :binary.bin_to_list(hash_table) } payload_path = Zorb.Config.payload_path(version) Zorb.Config.ensure_dirs!() File.write!(payload_path, :erlang.term_to_binary(payload)) # 3. Create Replacement ASTs host_registration_ast = quote(do: Orb.Import.register(Zorb.Capsule.Host)) global_block_ast = quote do global do @pc 0 @version unquote(version) @sp 0 @fp 0 @csp 0 @stack_base 0x90000 @call_stack_base 0x98000 @globals_base unquote(gb) @static_memory_base unquote(smb) @dictionary_base unquote(db) @object_table_base unquote(otb) @object_table_start unquote(if version <= 3, do: otb + 62, else: otb + 126) @abbreviations_base unquote(ab) @next_alphabet -1 @abbrev_mode 0 @recursion_depth 0 @packed_address_shift unquote(pas) @routine_offset unquote(ro) @string_offset unquote(so) @stream3_table 0 @stream3_active 0 @object_entry_size unquote(oes) @object_parent_offset unquote(po) @object_sibling_offset unquote(soj) @object_child_offset unquote(co) @object_property_table_offset unquote(pto) @random_state 1 @story_len unquote(byte_size(story_data)) @capabilities 0 @zscii_state 0 @zscii_high 0 @unicode_table_base 0 @current_font 1 @current_alphabet 0 @halted 0 @tick 0 end end memory_setup_ast = [ quote(do: Orb.Memory.pages(16)), quote do @payload File.read!(unquote(payload_path)) |> :erlang.binary_to_term() # Load story chunks for {off, list} <- @payload.story_chunks do Orb.Memory.initial_data!(off, u8: list) end Orb.Memory.initial_data!(0x80000, u8: @payload.unicode) Orb.Memory.initial_data!(0x81000, u8: unquote(alphabets)) Orb.Memory.initial_data!(0x82000, u8: @payload.hash) Orb.Memory.initial_data!(0x8A000, u8: unquote(:binary.bin_to_list(metadata_bin))) end ] ldict_definition_ast = quote do defw ldict(w1: I32, w2: I32, w3: I32), T.Address, slot: I32, addr: I32 do slot = I32.band(I32.xor(w1, I32.xor(w2, w3)), unquote(mask)) loop Search do addr = I32.add(0x82000, I32.shl(slot, 4)) if I32.eq(Memory.load!(I32, I32.add(addr, 12)), 0) do return(I32.const(0)) end if mdict(addr, w1, w2, w3) do return(Memory.load!(I32, I32.add(addr, 12))) end slot = I32.band(I32.add(slot, 1), unquote(mask)) Search.continue() end return(I32.const(0)) end end |> prune_version_branches(version) mdict_definition_ast = quote do defw mdict(addr: T.Address, w1: I32, w2: I32, w3: I32), I32, combined1: I32 do combined1 = I32.or(I32.shl(w1, 16), w2) if I32.ne(Memory.load!(I32, addr), combined1) do return(I32.const(0)) end if I32.ne(Memory.load!(I32, I32.add(addr, 4)), w3) do return(I32.const(0)) end return(I32.const(1)) end end |> prune_version_branches(version) do_tokenise_definition_ast = quote do defw do_tokenise(t: T.Address, p: T.Address, d: T.Address), text_start: I32, text_len: I32, max_words: I32, word_count: I32, i: I32, char: I32, in_word: I32, word_start: I32, word_len: I32, parse_idx: I32, dict_addr: T.Address, num_sep: I32, sep_addr: I32, j: I32, k: I32, sep_char: I32, is_sep: I32, zchars_len: I32, zchar: I32, w1: I32, w2: I32, w3: I32, z0: I32, z1: I32, z2: I32, z3: I32, z4: I32, z5: I32, z6: I32, z7: I32, z8: I32, alph_addr: I32, found: I32 do if I32.eq(d, 0), do: d = @dictionary_base if I32.ge_u(@version, 5) do text_start = I32.add(t, 2) text_len = read_byte(I32.add(t, 1)) else text_start = I32.add(t, 1) text_len = 0 Control.block FindNullBlock do loop FindNull do if I32.lt_u(text_len, 255) do if I32.eq(read_byte(I32.add(text_start, text_len)), 0) do FindNullBlock.break() end text_len = I32.add(text_len, 1) FindNull.continue() end end end end max_words = read_byte(p) word_count = 0 num_sep = read_byte(d) sep_addr = I32.add(d, 1) i = 0 in_word = 0 word_start = 0 word_len = 0 loop TokenLoop do if I32.lt_u(i, text_len) do char = read_byte(I32.add(text_start, i)) is_sep = 0 if I32.gt_u(num_sep, 0) do j = 0 Control.block SepLoopBlock do loop SepLoop do if I32.lt_u(j, num_sep) do sep_char = read_byte(I32.add(sep_addr, j)) if I32.eq(char, sep_char) do is_sep = 1 SepLoopBlock.break() end j = I32.add(j, 1) SepLoop.continue() end end end end if I32.or(I32.eq(char, 32), is_sep) do if in_word do if I32.lt_u(word_count, max_words) do z0 = 5 z1 = 5 z2 = 5 z3 = 5 z4 = 5 z5 = 5 z6 = 5 z7 = 5 z8 = 5 zchars_len = 0 j = 0 loop EncodeLoop do if I32.band( I32.lt_u(j, word_len), I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) ) do char = read_byte(I32.add(text_start, I32.add(word_start, j))) if I32.band(I32.ge_u(char, 65), I32.le_u(char, 90)) do char = I32.add(char, 32) end found = 0 if I32.eq(char, 32) do if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do zchar = 0 if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 end if I32.band( I32.eq(found, 0), I32.band(I32.ge_u(char, 97), I32.le_u(char, 122)) ) do zchar = I32.add(I32.sub(char, 97), 6) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 end if I32.eq(found, 0) do k = 0 Control.block A1Search do loop A1Loop do if I32.lt_u(k, 26) do if I32.eq(read_byte(I32.add(0x8101A, k)), char) do zchar = if(I32.le_u(@version, 2), do: I32.const(2), else: I32.const(4) ) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9) ) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end zchar = I32.add(k, 6) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9) ) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 A1Search.break() end k = I32.add(k, 1) A1Loop.continue() end end end end if I32.eq(found, 0) do alph_addr = 0x81034 if I32.ne(@version, 1) do alph_addr = 0x8104E end k = 0 Control.block A2Search do loop A2Loop do if I32.lt_u(k, 26) do if I32.eq(read_byte(I32.add(alph_addr, k)), char) do if I32.ne(k, 0) do zchar = if(I32.le_u(@version, 2), do: I32.const(3), else: I32.const(5) ) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9) ) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end zchar = I32.add(k, 6) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9) ) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 A2Search.break() end end k = I32.add(k, 1) A2Loop.continue() end end end end if I32.eq(found, 0) do zchar = if(I32.le_u(@version, 2), do: I32.const(3), else: I32.const(5)) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = 6 if I32.eq(zchars_len, 1), do: z1 = 6 if I32.eq(zchars_len, 2), do: z2 = 6 if I32.eq(zchars_len, 3), do: z3 = 6 if I32.eq(zchars_len, 4), do: z4 = 6 if I32.eq(zchars_len, 5), do: z5 = 6 if I32.eq(zchars_len, 6), do: z6 = 6 if I32.eq(zchars_len, 7), do: z7 = 6 if I32.eq(zchars_len, 8), do: z8 = 6 zchars_len = I32.add(zchars_len, 1) end if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do zchar = I32.band(I32.shr_u(char, 5), 0x1F) if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do zchar = I32.band(char, 0x1F) if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end end j = I32.add(j, 1) EncodeLoop.continue() end end w1 = I32.or(I32.shl(z0, 10), I32.or(I32.shl(z1, 5), z2)) w2 = I32.or(I32.shl(z3, 10), I32.or(I32.shl(z4, 5), z5)) w3 = 0 if I32.le_u(@version, 3) do w2 = I32.or(w2, 0x8000) else w3 = I32.or(I32.shl(z6, 10), I32.or(I32.shl(z7, 5), z8)) w3 = I32.or(w3, 0x8000) end dict_addr = ldict(w1, w2, w3) parse_idx = I32.add(p, I32.add(2, I32.mul(word_count, 4))) write_word(parse_idx, dict_addr) write_byte(I32.add(parse_idx, 2), word_len) write_byte( I32.add(parse_idx, 3), I32.add( word_start, if(I32.ge_u(@version, 5), do: I32.const(2), else: I32.const(1)) ) ) word_count = I32.add(word_count, 1) end in_word = 0 end if is_sep do if I32.lt_u(word_count, max_words) do char = read_byte(I32.add(text_start, i)) if I32.band(I32.ge_u(char, 65), I32.le_u(char, 90)) do char = I32.add(char, 32) end z0 = if(I32.le_u(@version, 2), do: I32.const(3), else: I32.const(5)) k = 0 z1 = 5 alph_addr = 0x81034 if I32.ne(@version, 1) do alph_addr = 0x8104E end loop SepFindLoop do if I32.lt_u(k, 26) do if I32.eq(read_byte(I32.add(alph_addr, k)), char) do z1 = I32.add(k, 6) else k = I32.add(k, 1) SepFindLoop.continue() end end end z2 = 5 z3 = 5 z4 = 5 z5 = 5 z6 = 5 z7 = 5 z8 = 5 w1 = I32.or(I32.shl(z0, 10), I32.or(I32.shl(z1, 5), z2)) w2 = I32.or(I32.shl(z3, 10), I32.or(I32.shl(z4, 5), z5)) w3 = 0 if I32.le_u(@version, 3) do w2 = I32.or(w2, 0x8000) else w3 = I32.or(I32.shl(z6, 10), I32.or(I32.shl(z7, 5), z8)) w3 = I32.or(w3, 0x8000) end dict_addr = ldict(w1, w2, w3) parse_idx = I32.add(p, I32.add(2, I32.mul(word_count, 4))) write_word(parse_idx, dict_addr) write_byte(I32.add(parse_idx, 2), 1) write_byte( I32.add(parse_idx, 3), I32.add(i, if(I32.ge_u(@version, 5), do: I32.const(2), else: I32.const(1))) ) word_count = I32.add(word_count, 1) end end else if I32.eq(in_word, 0) do word_start = i word_len = 0 in_word = 1 end word_len = I32.add(word_len, 1) end i = I32.add(i, 1) TokenLoop.continue() end end if in_word do if I32.lt_u(word_count, max_words) do z0 = 5 z1 = 5 z2 = 5 z3 = 5 z4 = 5 z5 = 5 z6 = 5 z7 = 5 z8 = 5 zchars_len = 0 j = 0 loop EncodeLoop2 do if I32.band( I32.lt_u(j, word_len), I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) ) do char = read_byte(I32.add(text_start, I32.add(word_start, j))) if I32.band(I32.ge_u(char, 65), I32.le_u(char, 90)) do char = I32.add(char, 32) end found = 0 if I32.eq(char, 32) do if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do zchar = 0 if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 end if I32.band(I32.eq(found, 0), I32.band(I32.ge_u(char, 97), I32.le_u(char, 122))) do zchar = I32.add(I32.sub(char, 97), 6) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 end if I32.eq(found, 0) do k = 0 Control.block A1Search2 do loop A1Loop2 do if I32.lt_u(k, 26) do if I32.eq(read_byte(I32.add(0x8101A, k)), char) do zchar = if(I32.le_u(@version, 2), do: I32.const(2), else: I32.const(4)) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end zchar = I32.add(k, 6) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 A1Search2.break() end k = I32.add(k, 1) A1Loop2.continue() end end end end if I32.eq(found, 0) do alph_addr = 0x81034 if I32.ne(@version, 1) do alph_addr = 0x8104E end k = 0 Control.block A2Search2 do loop A2Loop2 do if I32.lt_u(k, 26) do if I32.eq(read_byte(I32.add(alph_addr, k)), char) do if I32.ne(k, 0) do zchar = if(I32.le_u(@version, 2), do: I32.const(3), else: I32.const(5)) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end zchar = I32.add(k, 6) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end found = 1 A2Search2.break() end end k = I32.add(k, 1) A2Loop2.continue() end end end end if I32.eq(found, 0) do zchar = if(I32.le_u(@version, 2), do: I32.const(3), else: I32.const(5)) if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do if I32.eq(zchars_len, 0), do: z0 = 6 if I32.eq(zchars_len, 1), do: z1 = 6 if I32.eq(zchars_len, 2), do: z2 = 6 if I32.eq(zchars_len, 3), do: z3 = 6 if I32.eq(zchars_len, 4), do: z4 = 6 if I32.eq(zchars_len, 5), do: z5 = 6 if I32.eq(zchars_len, 6), do: z6 = 6 if I32.eq(zchars_len, 7), do: z7 = 6 if I32.eq(zchars_len, 8), do: z8 = 6 zchars_len = I32.add(zchars_len, 1) end if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do zchar = I32.band(I32.shr_u(char, 5), 0x1F) if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end if I32.lt_u( zchars_len, if(I32.le_u(@version, 3), do: I32.const(6), else: I32.const(9)) ) do zchar = I32.band(char, 0x1F) if I32.eq(zchars_len, 0), do: z0 = zchar if I32.eq(zchars_len, 1), do: z1 = zchar if I32.eq(zchars_len, 2), do: z2 = zchar if I32.eq(zchars_len, 3), do: z3 = zchar if I32.eq(zchars_len, 4), do: z4 = zchar if I32.eq(zchars_len, 5), do: z5 = zchar if I32.eq(zchars_len, 6), do: z6 = zchar if I32.eq(zchars_len, 7), do: z7 = zchar if I32.eq(zchars_len, 8), do: z8 = zchar zchars_len = I32.add(zchars_len, 1) end end j = I32.add(j, 1) EncodeLoop2.continue() end end w1 = I32.or(I32.shl(z0, 10), I32.or(I32.shl(z1, 5), z2)) w2 = I32.or(I32.shl(z3, 10), I32.or(I32.shl(z4, 5), z5)) w3 = 0 if I32.le_u(@version, 3) do w2 = I32.or(w2, 0x8000) else w3 = I32.or(I32.shl(z6, 10), I32.or(I32.shl(z7, 5), z8)) w3 = I32.or(w3, 0x8000) end dict_addr = ldict(w1, w2, w3) parse_idx = I32.add(p, I32.add(2, I32.mul(word_count, 4))) write_word(parse_idx, dict_addr) write_byte(I32.add(parse_idx, 2), word_len) write_byte( I32.add(parse_idx, 3), I32.add( word_start, if(I32.ge_u(@version, 5), do: I32.const(2), else: I32.const(1)) ) ) word_count = I32.add(word_count, 1) end end write_byte(I32.add(p, 1), word_count) # Clear remaining parse buffer slots write_word(I32.add(I32.add(p, 2), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 4), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 6), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 8), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 10), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 12), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 14), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 16), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 18), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 20), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 22), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 24), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 26), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 28), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 30), I32.mul(word_count, 4)), 0) write_word(I32.add(I32.add(p, 32), I32.mul(word_count, 4)), 0) end end |> prune_version_branches(version) # 4. Transform AST in a single pass final_ast = Macro.prewalk(ast, fn {:defmodule, meta, children} = node when is_list(children) -> [_old_name, args_list | _] = children body = case args_list do [{{:__block__, _, [:do]}, b}] -> b _ -> Keyword.get(args_list, :do) end case body do nil -> node _ -> alias_node = {:__aliases__, [alias: false], Module.split(module_name) |> Enum.map(&String.to_atom/1)} list_children = case body do {:__block__, _, list} -> list item -> [item] end processed_children = list_children |> Enum.reject(fn {{:., _, [target, func]}, _, _} when func in [:pages, :initial_data!] -> target_str = Macro.to_string(target) target_str == "Memory" or target_str == "Orb.Memory" _ -> false end) |> Enum.map(fn {:global, _, _} -> global_block_ast {:defw, _, [name_node | _]} = node -> name = case name_node do {name, _, _} when is_atom(name) -> name name when is_atom(name) -> name _ -> nil end case name do :ldict -> ldict_definition_ast :mdict -> mdict_definition_ast :do_tokenise -> do_tokenise_definition_ast _ -> node end node -> node end) final_children = inject_after_use_orb( processed_children, {:__block__, [], [host_registration_ast | memory_setup_ast]} ) |> Enum.reject(&is_nil/1) {:defmodule, meta, [alias_node, [do: {:__block__, [], final_children}]]} end node -> node end) source_code = Sourceror.to_string(final_ast) {source_code, nil} end def prune_version_branches(ast, version) do result = Macro.prewalk(ast, fn {:defmodule, meta, args} -> new_args = Enum.map(args, fn [{{:__block__, _, [:do]}, body}] -> [{{:__block__, [], [:do]}, prune_module_body(body, version)}] [do: body] -> [do: prune_module_body(body, version)] other -> other end) {:defmodule, meta, new_args} {func_type, _, _} = node when func_type in [:defw, :defwp] -> prune_function(node, version) node -> node end) # Remove any nil nodes Macro.prewalk(result, fn nil -> {:__block__, [], []} node -> node end) end defp prune_module_body(body, version) do case body do {:__block__, meta, children} -> pruned_children = Enum.map(children, &prune_function(&1, version)) {:__block__, meta, pruned_children} other -> other end end defp prune_function({func_type, func_meta, args}, version) when func_type in [:defw, :defwp] do pruned_args = case args do # 4 args: [name, return_type, local_vars, body_kwlist] [name, return_type, local_vars, body_kwlist] when is_list(body_kwlist) -> [name, return_type, local_vars, prune_kwlist(body_kwlist, version)] # 3 args: [name, return_type, body_kwlist] [name, return_type, body_kwlist] when is_list(body_kwlist) -> [name, return_type, prune_kwlist(body_kwlist, version)] # 2 args: [name, body_kwlist] [name, body_kwlist] when is_list(body_kwlist) -> [name, prune_kwlist(body_kwlist, version)] # Fallback: try to find and prune any keyword list other_args -> Enum.map(other_args, fn kwlist when is_list(kwlist) -> case Keyword.keyword?(kwlist) do true -> prune_kwlist(kwlist, version) false -> kwlist end arg -> arg end) end {func_type, func_meta, pruned_args} end defp prune_function(other, _version), do: other defp prune_kwlist(kwlist, version) do Enum.map(kwlist, fn {{:__block__, _, [:do]}, func_body} -> pruned = prune_version_branches_in_block(func_body, version) cleaned = clean_empty_statements(pruned) {{:__block__, [], [:do]}, cleaned} {key, func_body} when key == :do -> pruned = prune_version_branches_in_block(func_body, version) cleaned = clean_empty_statements(pruned) {key, cleaned} other -> other end) end # Remove empty blocks that appear as statements, but preserve them in if/else bodies defp clean_empty_statements(ast) do ast |> flatten_nested_blocks() |> remove_empty_blocks_from_lists() end # Flatten blocks that contain other blocks as direct children defp flatten_nested_blocks(ast) do Macro.prewalk(ast, fn {:__block__, meta, children} when is_list(children) -> flattened = Enum.flat_map(children, fn {:__block__, _, nested_children} when is_list(nested_children) -> nested_children other -> [other] end) {:__block__, meta, flattened} node -> node end) end # Remove empty blocks from statement lists defp remove_empty_blocks_from_lists(ast) do Macro.postwalk(ast, fn {:__block__, meta, children} when is_list(children) -> filtered = Enum.reject(children, &empty_block?/1) {:__block__, meta, filtered} node -> node end) end defp empty_block?({:__block__, _, []}), do: true defp empty_block?(nil), do: true defp empty_block?(_), do: false defp prune_version_branches_in_block(nil, _version), do: {:__block__, [], []} defp prune_version_branches_in_block(body, version) do Macro.postwalk(body, fn {:if, meta, [condition, blocks]} -> case condition do {{:., _, [target, op]}, _, [{:@, _, [{v_name, _, _}]}, v]} when op in [:ge_u, :le_u, :lt_u, :gt_u, :eq, :ne] and v_name == :version -> target_str = Macro.to_string(target) if target_str in ["I32", "Orb.I32"] do # Extract version number from AST version_num = case v do {{:., _, [{:__aliases__, _, [:I32]}, :const]}, _, [{:__block__, _, [n]}]} when is_integer(n) -> n {{:., _, [:I32, :const]}, _, [{:__block__, _, [n]}]} when is_integer(n) -> n {{:., _, [{:__aliases__, _, [:I32]}, :const]}, _, [n]} when is_integer(n) -> n {{:., _, [:I32, :const]}, _, [n]} when is_integer(n) -> n {:__block__, _, [n]} when is_integer(n) -> n n when is_integer(n) -> n _ -> nil end if version_num do # Sourceror wraps keys in {:__block__, meta, [key]} yes = Enum.find_value(blocks, {:__block__, [], []}, fn {{:__block__, _, [:do]}, value} -> value {:do, value} -> value _ -> nil end) no = Enum.find_value(blocks, {:__block__, [], []}, fn {{:__block__, _, [:else]}, value} -> value {:else, value} -> value _ -> nil end) take_yes = case op do :ge_u -> version >= version_num :le_u -> version <= version_num :lt_u -> version < version_num :gt_u -> version > version_num :eq -> version == version_num :ne -> version != version_num end result = if take_yes, do: yes, else: no # Don't unwrap - let parent context handle it result else {:if, meta, [condition, blocks]} end else {:if, meta, [condition, blocks]} end _ -> {:if, meta, [condition, blocks]} end node -> node end) end defp inject_after_use_orb(children, new_setup) do index = Enum.find_index(children, fn {:use, _, [{:__aliases__, _, [:Orb]}]} -> true _ -> false end) setup_children = case new_setup do {:__block__, _, list} -> list item -> [item] end case index do nil -> setup_children ++ children i -> {head, tail} = Enum.split(children, i + 1) head ++ setup_children ++ tail end end def extract_header_fields(story_data) do <<_v::8, _f1::8, _rel::16, _hmb::16, _pc::16, dictionary_base::16, object_table_base::16, globals_base::16, static_memory_base::16, _f2::16, _serial::binary-size(6), abbreviations_base::16, _rest::binary>> = story_data {globals_base, static_memory_base, dictionary_base, abbreviations_base, object_table_base} end def calculate_version_constants(version) when version <= 3, do: {1, 9, 4, 5, 6, 7} def calculate_version_constants(_version), do: {2, 14, 6, 8, 10, 12} def calculate_offsets(version, story_data) when version in 6..7 do <<_::320, r::16, s::16, _::binary>> = story_data {r * 8, s * 8} end def calculate_offsets(_version, _story_data), do: {0, 0} def prune_story_data(data, _opts), do: data def generate_dictionary_hash_table(story_data, dict_base, version) do <<_::binary-size(dict_base), num_sep::8, _::binary>> = story_data header_end = dict_base + 1 + num_sep <<_::binary-size(header_end), entry_len::8, num_entries::16, _::binary>> = story_data entries_start = header_end + 3 table_size = 2048 mask = table_size - 1 table = Tuple.duplicate({0, 0, 0, 0}, table_size) final_table = case num_entries do 0 -> table _ -> Enum.reduce(0..(num_entries - 1), table, fn i, acc -> addr = entries_start + i * entry_len {w1, w2, w3} = get_encoded_words(story_data, addr, version) hash = Bitwise.bxor(w1, Bitwise.bxor(w2, w3)) |> Bitwise.band(mask) insert_at_slot(acc, hash, w1, w2, w3, addr, mask) end) end bin = for i <- 0..(table_size - 1), into: <<>> do {w1, w2, w3, addr} = elem(final_table, i) combined1 = Bitwise.bor(Bitwise.bsl(w1, 16), w2) <<combined1::32-little, w3::32-little, 0::32-little, addr::32-little>> end {bin, mask} end defp get_encoded_words(story_data, addr, version) when version <= 3 do <<_::binary-size(addr), w1::16-big, w2::16-big, _::binary>> = story_data {w1, w2, 0} end defp get_encoded_words(story_data, addr, _version) do <<_::binary-size(addr), w1::16-big, w2::16-big, w3::16-big, _::binary>> = story_data {w1, w2, w3} end defp insert_at_slot(table, slot, w1, w2, w3, addr, mask) do case elem(table, slot) do {0, 0, 0, 0} -> put_elem(table, slot, {w1, w2, w3, addr}) _ -> insert_at_slot(table, Bitwise.band(slot + 1, mask), w1, w2, w3, addr, mask) end end def generate_unicode_binary do table = [ 0x00E4, 0x00F6, 0x00FC, 0x00C4, 0x00D6, 0x00DC, 0x00DF, 0x00BB, 0x00AB, 0x00EB, 0x00EF, 0x00FF, 0x00CB, 0x00CF, 0x00E1, 0x00E9, 0x00ED, 0x00F3, 0x00FA, 0x00FD, 0x00C1, 0x00C9, 0x00CD, 0x00D3, 0x00DA, 0x00DD, 0x00E0, 0x00E8, 0x00EC, 0x00F2, 0x00F9, 0x00C0, 0x00C8, 0x00CC, 0x00D2, 0x00D9, 0x00E2, 0x00EA, 0x00EE, 0x00F4, 0x00FB, 0x00C2, 0x00CA, 0x00CE, 0x00D4, 0x00DB, 0x00E5, 0x00C5, 0x00F8, 0x00D8, 0x00E3, 0x00F1, 0x00F5, 0x00C3, 0x00D1, 0x00D5, 0x00E6, 0x00C6, 0x00E7, 0x00C7, 0x00FE, 0x00F0, 0x00DE, 0x00D0, 0x00A3, 0x0153, 0x0152, 0x00A1, 0x00BF, 0x00AA, 0x00BA, 0x00E6, 0x00C6, 0x00F8, 0x00D8, 0x00E5, 0x00C5, 0x00E7, 0x00C7, 0x00F0, 0x00D0, 0x00F1, 0x00D1, 0x00F5, 0x00D5, 0x00FE, 0x00DE, 0x00A9, 2122, 0x20AC, 0x0024, 0x0192, 0x03B1, 0x03B2, 0x03B3, 0x03B4, 0x03B5, 0x03B6, 0x03B7, 0x03B8, 0x03B9, 0x03BA, 0x03BB, 0x03BC, 0x03BD, 0x03BE, 0x03BF, 0x03C0, 0x03C1, 0x03C2, 0x03C3, 0x03C4, 0x03C5, 0x03C6, 0x03C7, 0x03C8, 0x03C9, 0x0391, 0x0392, 0x0393, 0x0394, 0x0395, 0x0396, 0x0397, 0x0398, 0x0399, 0x039A, 0x039B, 0x039C, 0x039D, 0x039E, 0x039F, 0x03A0, 0x03A1, 0x03A3, 0x03A4, 0x03A5, 0x03A6, 0x03A7, 0x03A8, 0x03A9 ] for u <- table, into: <<>>, do: <<u::16-big>> endend