Skip to content

Native code clinic — follow a String to QBE

Run the Native executable trace first. This clinic follows its String literal and puts call through the TypeRB-authored frontend into two small QBE products: static data and an exported entry function.

This correspondence assumes the trace’s default downloaded seed. A compiler supplied through TYPE_RB_NATIVE_COMPILER can represent different source.

def main()
puts("hello from TypeRB Native")
return
end

The number 7 is a token-derived symbol in this snapshot. It is not a stable public name. Search for the data declaration or g4_puts handoff, not for the exact $g4s7 spelling in a future revision.

The lexer enters gate4_scan_string after seeing ". It collects characters, decodes supported escapes, and stores a token with kind 3:

def gate4_scan_string(mut state: Gate4Compiler, source: String, mut position: Array<Integer>, line: Integer)
# scan characters and supported escapes
if closed
gate4_push_token(state, 3, text, line)
else
gate4_diagnostic(state, "TRBN4001", line, "unterminated String literal")
end
end

Read the exact gate4_scan_string implementation. Its output is decoded text plus a source line. It has not yet established that the surrounding call is well typed.

Small TypeRB note: mut marks bindings or parameters that may change. Array<Integer> and small records are used as explicit compiler storage; they are implementation mechanisms, not new source-language phases.

2. Parsing accepts a String as a primary expression

Section titled “2. Parsing accepts a String as a primary expression”

This seed’s parser validates expression syntax over token storage. A kind-3 token is a complete primary expression, so parsing advances once:

if kind == 3
gate4_advance(position)
else
# grouped expressions and other primary forms
end

The branch lives in gate4_parse_primary_syntax. Unlike the reference compiler clinic, this path does not need a separate public AST node for the lesson. The durable responsibility is still the same: accept the authored shape before checking its meaning.

The checker gives a kind-3 primary the type String:

if kind == 3
gate4_advance(position)
return gate4_checked_value("String", 0, "", 0)
end

See gate4_check_primary. The call checker then treats puts as a one-argument function returning Void and expects that argument to be String:

if callee.name == "puts"
function_index =- 1
result_type = "Void"
end
# while checking the argument
if callee.name == "puts"
argument_expected = "String"
end

Read the complete gate4_check_call branch. Changing the fixture to puts(42) should therefore stop before QBE emission with a type diagnostic.

4. Emission gives the String a data identity

Section titled “4. Emission gives the String a data identity”

Before functions are emitted, gate4_emit_static_strings walks String tokens, deduplicates equal values when appropriate, and writes a QBE data declaration:

gate4_output_line(
output,
"data $g4s" + gate4_integer_string(token_index) +
" = align 8 { l " + gate4_integer_string(value.size()) + ",",
)

The static String emitter then emits the bytes and a trailing zero. For this fixture, l 24 records the 24-byte content length and the b items spell the UTF-8-compatible ASCII text.

When the expression emitter sees the same String token, it returns the matching global symbol as a checked String value:

if kind == 3
gate4_advance(position)
return gate4_emitted_value("$g4s" + gate4_integer_string(canonical), "String", 0, "", 0, "")
end

See gate4_emit_primary. The data producer and expression consumer agree on one canonical symbol.

The direct-call emitter turns the checked puts call into:

if callee.name == "puts"
gate4_output_line(output, "\tcall $g4_puts(" + arguments + ")")
return gate4_emitted_value("", "Void", 0, "", 0, "")
end

That branch is in gate4_emit_direct_call. The runtime’s $g4_puts function loads the length, advances eight bytes to the content, writes it, and writes a newline.

Finally, the ordinary application entry finds the source main, calls its emitted function, and returns status zero:

gate4_output_line(output, "export function w $main(w %argc, l %argv) {")
gate4_output_line(output, "@start")
main_index := gate4_find_function_in_module(state, state.entry_module[0], "main")
gate4_output_line(output, "\tcall $g4f" + gate4_integer_string(main_index) + "()")
gate4_output_line(output, "\tret 0")

Read the exact main emission.

You only need six pieces of notation:

QBE spelling Meaning here
data $g4s7 a global data definition; $ names a global symbol
l a 64-bit long value, used here for lengths and pointers
w a 32-bit word value, used for the process status and argc
b individual bytes in static data
%argc a function-local temporary; % names local values
@start, call, ret a basic-block label, a call, and a return

Learn more QBE only when a code path introduces another operation. This keeps the backend notation attached to the TypeRB value whose behavior you already understand.

Run focused evidence in a current checkout

Section titled “Run focused evidence in a current checkout”

The clinic explains the immutable seed source. For a new Native change, use the commands owned by your current checkout:

Terminal window
trb check --config compiler/gate4/trbconfig.jsonc
TYPE_RB_NATIVE_ROOT="$PWD" trb test --config compiler/gate4/trbconfig.jsonc

Then run the contributor-guide trace to observe the fixed seed boundary again. Do not treat agreement with this older seed as proof that a current bootstrap chain or compatibility claim passes.

  1. Which stage first says the literal has type String?
  2. Why do the static-data emitter and expression emitter need the same canonical symbol?
  3. What additional claim would require more than the QBE emitted by this trace?
Check your answers
  1. The checker; the lexer preserves decoded text and the parser accepts its syntax first.
  2. The function call must refer to the exact data object that contains the literal’s length and bytes.
  3. Running native code, proving a runtime or ABI change, claiming target portability, or proving self-hosting would require the corresponding QBE, toolchain, conformance, target, or multi-generation evidence.

Continue with the workflow and test matrix and the Native change journey. Use the big map to find the corresponding responsibilities in the newer implementation snapshot.