Merge branch 'native_boot'
This commit is contained in:
File diff suppressed because one or more lines are too long
53613
boot/qbe_emit.cm.mcode
53613
boot/qbe_emit.cm.mcode
File diff suppressed because one or more lines are too long
2
help.ce
2
help.ce
@@ -123,4 +123,4 @@ log.console("")
|
||||
log.console("Run 'cell help <command>' for more information.")
|
||||
}
|
||||
|
||||
$stop()
|
||||
$$stop()
|
||||
|
||||
@@ -147,21 +147,38 @@ function compile_native_cached(name, source_path) {
|
||||
return
|
||||
}
|
||||
|
||||
var t0 = null
|
||||
var t1 = null
|
||||
|
||||
os.print("bootstrap: compiling native: " + name + "\n")
|
||||
t0 = os.now()
|
||||
ast = analyze(src, source_path)
|
||||
compiled = streamline_mod(mcode_mod(ast))
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] pipeline (tok+parse+fold+mcode+streamline): " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
|
||||
t0 = os.now()
|
||||
il_parts = _qbe_emit_mod(compiled, _qbe_mod, null)
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] qbe_emit: " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
|
||||
helpers_il = (il_parts.helpers && length(il_parts.helpers) > 0)
|
||||
? text(il_parts.helpers, "\n") : ""
|
||||
all_fns = text(il_parts.functions, "\n")
|
||||
full_il = il_parts.data + "\n\n" + helpers_il + "\n\n" + all_fns
|
||||
|
||||
t0 = os.now()
|
||||
asm_text = os.qbe(full_il)
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] os.qbe (QBE compile): " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
|
||||
tmp = '/tmp/cell_boot_' + name
|
||||
fd.slurpwrite(tmp + '.s', stone(blob(asm_text)))
|
||||
|
||||
t0 = os.now()
|
||||
rc = os.system(_cc + ' -c ' + tmp + '.s -o ' + tmp + '.o')
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] clang -c: " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
if (rc != 0) {
|
||||
os.print("error: assembly failed for " + name + "\n")
|
||||
disrupt
|
||||
@@ -186,7 +203,10 @@ function compile_native_cached(name, source_path) {
|
||||
link_cmd = link_cmd + ' -undefined dynamic_lookup'
|
||||
link_cmd = link_cmd + ' ' + tmp + '.o ' + rt_o + ' -o ' + dylib_path
|
||||
|
||||
t0 = os.now()
|
||||
rc = os.system(link_cmd)
|
||||
t1 = os.now()
|
||||
os.print(" [" + name + "] clang -shared (link): " + text((t1 - t0) / 1000000) + "ms\n")
|
||||
if (rc != 0) {
|
||||
os.print("error: linking failed for " + name + "\n")
|
||||
disrupt
|
||||
|
||||
440
qbe_emit.cm
440
qbe_emit.cm
@@ -1073,6 +1073,7 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
var cmp_id = 0
|
||||
var depth = 0
|
||||
var d = 0
|
||||
var handler = null
|
||||
|
||||
// Pre-scan: count invoke/tail_invoke points to assign segment numbers.
|
||||
// Must skip dead code (instructions after terminators) the same way
|
||||
@@ -1385,51 +1386,25 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
return `%${np}_iw`
|
||||
}
|
||||
|
||||
// Walk instructions
|
||||
var last_was_term = false
|
||||
i = 0
|
||||
while (i < length(instrs)) {
|
||||
instr = instrs[i]
|
||||
instr_idx = i
|
||||
// --- Opcode handlers (record dispatch) ---
|
||||
var handlers = {}
|
||||
|
||||
// Emit @disruption_handler at the right flat index
|
||||
// disruption_pc counts all entries (labels + instructions)
|
||||
if (has_handler && i == disruption_pc) {
|
||||
if (!last_was_term) {
|
||||
emit(" jmp @disruption_handler")
|
||||
handlers["int"] = function() {
|
||||
s_write(a1, text(a2 * 2))
|
||||
}
|
||||
emit("@disruption_handler")
|
||||
emit(" call $cell_rt_clear_exception(l %ctx)")
|
||||
emit(` %fp =l call $cell_rt_refresh_fp(l %ctx)`)
|
||||
last_was_term = false
|
||||
in_handler = true
|
||||
handlers["null"] = function() {
|
||||
s_write(a1, text(qbe.js_null))
|
||||
}
|
||||
i = i + 1
|
||||
|
||||
// Labels are plain strings; skip nop pseudo-labels from streamline
|
||||
if (is_text(instr)) {
|
||||
if (starts_with(instr, "_nop_")) continue
|
||||
lbl = sanitize(instr)
|
||||
if (!last_was_term) {
|
||||
emit(` jmp @${lbl}`)
|
||||
handlers["true"] = function() {
|
||||
s_write(a1, text(qbe.js_true))
|
||||
}
|
||||
emit("@" + lbl)
|
||||
last_was_term = false
|
||||
continue
|
||||
handlers["false"] = function() {
|
||||
s_write(a1, text(qbe.js_false))
|
||||
}
|
||||
|
||||
// Skip dead code: non-label instructions after a terminator are unreachable
|
||||
if (last_was_term) continue
|
||||
|
||||
op = instr[0]
|
||||
a1 = instr[1]
|
||||
a2 = instr[2]
|
||||
a3 = instr[3]
|
||||
last_was_term = false
|
||||
|
||||
handlers["access"] = function() {
|
||||
// Peephole: inline `text(x)` intrinsic call sequence
|
||||
// access text; frame; null this; setarg 0 this; setarg 1 x; invoke
|
||||
if (op == "access" && is_object(a2) && a2.make == "intrinsic" && a2.name == "text") {
|
||||
if (is_object(a2) && a2.make == "intrinsic" && a2.name == "text") {
|
||||
if (instr_idx + 5 < length(instrs)) {
|
||||
peek1 = instrs[instr_idx + 1]
|
||||
peek2 = instrs[instr_idx + 2]
|
||||
@@ -1454,31 +1429,12 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
refresh_fp()
|
||||
s_write(text_dest_slot, `%${p}_r`)
|
||||
i = instr_idx + 6
|
||||
continue
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
if (op == "int") {
|
||||
s_write(a1, text(a2 * 2))
|
||||
continue
|
||||
}
|
||||
if (op == "null") {
|
||||
s_write(a1, text(qbe.js_null))
|
||||
continue
|
||||
}
|
||||
if (op == "true") {
|
||||
s_write(a1, text(qbe.js_true))
|
||||
continue
|
||||
}
|
||||
if (op == "false") {
|
||||
s_write(a1, text(qbe.js_false))
|
||||
continue
|
||||
}
|
||||
if (op == "access") {
|
||||
if (is_number(a2)) {
|
||||
if (is_integer(a2)) {
|
||||
s_write(a1, text(a2 * 2))
|
||||
@@ -1520,20 +1476,12 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
} else {
|
||||
s_write(a1, text(qbe.js_null))
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Movement ---
|
||||
|
||||
if (op == "move") {
|
||||
handlers.move = function() {
|
||||
v = s_read(a2)
|
||||
s_write(a1, v)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Generic arithmetic (VM dispatches int/float) ---
|
||||
|
||||
if (op == "add") {
|
||||
handlers.add = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1562,9 +1510,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "subtract") {
|
||||
handlers.subtract = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1593,9 +1540,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "multiply") {
|
||||
handlers.multiply = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1624,9 +1570,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "divide") {
|
||||
handlers.divide = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1635,9 +1580,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_rd =d div ${lhs_d}, ${rhs_d}`)
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
continue
|
||||
}
|
||||
if (op == "modulo") {
|
||||
handlers.modulo = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1661,9 +1605,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(`@${p}_bad`)
|
||||
s_write(a1, text(qbe.js_null))
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "remainder") {
|
||||
handlers.remainder = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1682,9 +1625,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(`@${p}_bad`)
|
||||
s_write(a1, text(qbe.js_null))
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "max" || op == "min") {
|
||||
var max_min_handler = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1704,18 +1646,18 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(`@${p}_done_math`)
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
continue
|
||||
}
|
||||
if (op == "abs") {
|
||||
handlers.max = max_min_handler
|
||||
handlers.min = max_min_handler
|
||||
handlers.abs = function() {
|
||||
lhs = s_read(a2)
|
||||
p = fresh()
|
||||
lhs_d = emit_num_to_double(lhs)
|
||||
emit(` %${p}_rd =d call $fabs(d ${lhs_d})`)
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
continue
|
||||
}
|
||||
if (op == "sign") {
|
||||
handlers.sign = function() {
|
||||
lhs = s_read(a2)
|
||||
p = fresh()
|
||||
lhs_d = emit_num_to_double(lhs)
|
||||
@@ -1733,9 +1675,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(`@${p}_zero`)
|
||||
s_write(a1, text(0))
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "fraction") {
|
||||
handlers.fraction = function() {
|
||||
lhs = s_read(a2)
|
||||
p = fresh()
|
||||
lhs_d = emit_num_to_double(lhs)
|
||||
@@ -1743,18 +1684,16 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_rd =d sub ${lhs_d}, %${p}_ti`)
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
continue
|
||||
}
|
||||
if (op == "integer") {
|
||||
handlers.integer = function() {
|
||||
lhs = s_read(a2)
|
||||
p = fresh()
|
||||
lhs_d = emit_num_to_double(lhs)
|
||||
emit(` %${p}_rd =d call $trunc(d ${lhs_d})`)
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
continue
|
||||
}
|
||||
if (op == "floor" || op == "ceiling" || op == "round" || op == "trunc") {
|
||||
var floor_ceil_round_trunc_handler = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1829,19 +1768,20 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "negate") {
|
||||
handlers.floor = floor_ceil_round_trunc_handler
|
||||
handlers.ceiling = floor_ceil_round_trunc_handler
|
||||
handlers.round = floor_ceil_round_trunc_handler
|
||||
handlers.trunc = floor_ceil_round_trunc_handler
|
||||
handlers.negate = function() {
|
||||
lhs = s_read(a2)
|
||||
p = fresh()
|
||||
lhs_d = emit_num_to_double(lhs)
|
||||
emit(` %${p}_rd =d neg ${lhs_d}`)
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (op == "pow") {
|
||||
handlers.pow = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -1850,17 +1790,12 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_rd =d call $pow(d ${lhs_d}, d ${rhs_d})`)
|
||||
emit(` %${p}_r =l call $qbe_new_float64(l %ctx, d %${p}_rd)`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- String concat ---
|
||||
|
||||
if (op == "concat") {
|
||||
handlers.concat = function() {
|
||||
emit(` %fp =l call $__concat_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "stone_text") {
|
||||
handlers.stone_text = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
emit(` %${p}_ptag =l and ${v}, 7`)
|
||||
@@ -1880,66 +1815,53 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_new_hdr =l or %${p}_hdr, 8`)
|
||||
emit(` storel %${p}_new_hdr, %${p}_ptr`)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Type checks — use qbe.cm macros (no GC, no refresh) ---
|
||||
|
||||
if (op == "is_int") {
|
||||
handlers.is_int = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_tag =l and ${v}, 1`)
|
||||
emit(` %${p}_w =w ceql %${p}_tag, 0`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_text") {
|
||||
handlers.is_text = function() {
|
||||
v = s_read(a2)
|
||||
s_write(a1, emit_pack_bool_js(emit_is_text_w(v)))
|
||||
continue
|
||||
}
|
||||
if (op == "is_num") {
|
||||
handlers.is_num = function() {
|
||||
v = s_read(a2)
|
||||
s_write(a1, emit_pack_bool_js(emit_is_num_w(v)))
|
||||
continue
|
||||
}
|
||||
if (op == "is_bool") {
|
||||
handlers.is_bool = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_t5 =l and ${v}, 31`)
|
||||
emit(` %${p}_w =w ceql %${p}_t5, 3`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_null") {
|
||||
handlers.is_null = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_t5 =l and ${v}, 31`)
|
||||
emit(` %${p}_w =w ceql %${p}_t5, 7`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_identical") {
|
||||
handlers.is_identical = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
emit(` %${p}_w =w ceql ${lhs}, ${rhs}`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_array") {
|
||||
handlers.is_array = function() {
|
||||
emit(` call $__is_array_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "is_func") {
|
||||
handlers.is_func = function() {
|
||||
emit(` call $__is_func_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "is_record") {
|
||||
handlers.is_record = function() {
|
||||
emit(` call $__is_record_ss(l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "is_stone") {
|
||||
handlers.is_stone = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_ptag =l and ${v}, 7`)
|
||||
@@ -1964,9 +1886,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_w =w copy 1`)
|
||||
emit(`@${p}_done`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_proxy") {
|
||||
handlers.is_proxy = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_ptag =l and ${v}, 7`)
|
||||
@@ -1996,9 +1917,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_w =w copy 0`)
|
||||
emit(`@${p}_done`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_blob") {
|
||||
handlers.is_blob = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_ptag =l and ${v}, 7`)
|
||||
@@ -2022,9 +1942,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_w =w copy 0`)
|
||||
emit(`@${p}_done`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_data") {
|
||||
handlers.is_data = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_ptag =l and ${v}, 7`)
|
||||
@@ -2053,9 +1972,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_w =w copy 0`)
|
||||
emit(`@${p}_done`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_fit") {
|
||||
handlers.is_fit = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_tag =l and ${v}, 1`)
|
||||
@@ -2082,9 +2000,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_w =w copy 0`)
|
||||
emit(`@${p}_done`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_char") {
|
||||
handlers.is_char = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_imm =l and ${v}, 31`)
|
||||
@@ -2122,10 +2039,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_w =w copy 0`)
|
||||
emit(`@${p}_done`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_digit" || op == "is_letter" ||
|
||||
op == "is_lower" || op == "is_upper" || op == "is_ws") {
|
||||
var is_char_class_handler = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_imm =l and ${v}, 31`)
|
||||
@@ -2214,33 +2129,31 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_w =w copy 0`)
|
||||
emit(`@${p}_done`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_true") {
|
||||
handlers.is_digit = is_char_class_handler
|
||||
handlers.is_letter = is_char_class_handler
|
||||
handlers.is_lower = is_char_class_handler
|
||||
handlers.is_upper = is_char_class_handler
|
||||
handlers.is_ws = is_char_class_handler
|
||||
handlers.is_true = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_w =w ceql ${v}, ${text(qbe.js_true)}`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_false") {
|
||||
handlers.is_false = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_w =w ceql ${v}, ${text(qbe.js_false)}`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
if (op == "is_actor") {
|
||||
handlers.is_actor = function() {
|
||||
v = s_read(a2)
|
||||
p = fresh()
|
||||
emit(` %${p}_w =w call $cell_rt_is_actor(l %ctx, l ${v})`)
|
||||
s_write(a1, emit_pack_bool_js(`%${p}_w`))
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Comparisons (int path, no GC) ---
|
||||
|
||||
if (op == "eq" || op == "ne" || op == "lt" || op == "le" || op == "gt" || op == "ge") {
|
||||
var cmp_handler = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -2289,22 +2202,23 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(`@${p}_ok`)
|
||||
s_write(a1, `%${p}_r`)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
|
||||
if (op == "eq_tol" || op == "ne_tol") {
|
||||
handlers.eq = cmp_handler
|
||||
handlers.ne = cmp_handler
|
||||
handlers.lt = cmp_handler
|
||||
handlers.le = cmp_handler
|
||||
handlers.gt = cmp_handler
|
||||
handlers.ge = cmp_handler
|
||||
var eq_ne_tol_handler = function() {
|
||||
a4 = instr[4]
|
||||
emit(` call $__${op}_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)}, l ${text(a4)})`)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Boolean ops ---
|
||||
|
||||
if (op == "not") {
|
||||
handlers.eq_tol = eq_ne_tol_handler
|
||||
handlers.ne_tol = eq_ne_tol_handler
|
||||
handlers.not = function() {
|
||||
emit(` call $__not_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "and") {
|
||||
handlers["and"] = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -2316,9 +2230,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(`@${p}_f`)
|
||||
s_write(a1, lhs)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "or") {
|
||||
handlers["or"] = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -2330,28 +2243,20 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(`@${p}_f`)
|
||||
s_write(a1, rhs)
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Bitwise ops — use qbe.cm macros (no GC) ---
|
||||
|
||||
if (op == "bitnot") {
|
||||
handlers.bitnot = function() {
|
||||
emit(` call $__bnot_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "bitand") {
|
||||
handlers.bitand = function() {
|
||||
emit(` call $__band_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "bitor") {
|
||||
handlers.bitor = function() {
|
||||
emit(` call $__bor_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "bitxor") {
|
||||
handlers.bitxor = function() {
|
||||
emit(` call $__bxor_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
continue
|
||||
}
|
||||
if (op == "shl") {
|
||||
handlers.shl = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -2376,9 +2281,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` ret 15`)
|
||||
}
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "shr") {
|
||||
handlers.shr = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -2403,9 +2307,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` ret 15`)
|
||||
}
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "ushr") {
|
||||
handlers.ushr = function() {
|
||||
lhs = s_read(a2)
|
||||
rhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -2430,12 +2333,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` ret 15`)
|
||||
}
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Property access — runtime calls [G] ---
|
||||
|
||||
if (op == "load_field") {
|
||||
handlers.load_field = function() {
|
||||
pn = null
|
||||
if (is_text(a3)) {
|
||||
pn = a3
|
||||
@@ -2453,14 +2352,12 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %fp =l call $__load_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
}
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "load_index") {
|
||||
handlers.load_index = function() {
|
||||
emit(` %fp =l call $__load_index_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "load_dynamic") {
|
||||
handlers.load_dynamic = function() {
|
||||
pn = null
|
||||
if (is_text(a3)) {
|
||||
pn = a3
|
||||
@@ -2478,9 +2375,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %fp =l call $__load_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
}
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "store_field") {
|
||||
handlers.store_field = function() {
|
||||
// IR: ["store_field", obj, val, prop]
|
||||
pn = null
|
||||
if (is_text(a3)) {
|
||||
@@ -2499,9 +2395,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %fp =l call $__store_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
}
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "store_index") {
|
||||
handlers.store_index = function() {
|
||||
// IR: ["store_index", obj, val, idx]
|
||||
lhs = s_read(a1)
|
||||
rhs = s_read(a2)
|
||||
@@ -2563,9 +2458,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %fp =l call $__store_index_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
emit_exc_check()
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "store_dynamic") {
|
||||
handlers.store_dynamic = function() {
|
||||
// IR: ["store_dynamic", obj, val, key]
|
||||
pn = null
|
||||
if (is_text(a3)) {
|
||||
@@ -2584,12 +2478,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %fp =l call $__store_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
}
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Closure access (no GC) ---
|
||||
|
||||
if (op == "get") {
|
||||
handlers.get = function() {
|
||||
// mcode: get(dest, slot, depth) — a2=slot, a3=depth
|
||||
depth = a3
|
||||
if (depth == 0) {
|
||||
@@ -2613,9 +2503,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_val =l loadl %${p}_slotp`)
|
||||
s_write(a1, `%${p}_val`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (op == "put") {
|
||||
handlers.put = function() {
|
||||
// mcode: put(val, slot, depth) — a2=slot, a3=depth
|
||||
v = s_read(a1)
|
||||
depth = a3
|
||||
@@ -2638,12 +2527,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_slotp =l add %${p}_fp, ${text(a2 * 8)}`)
|
||||
emit(` storel ${v}, %${p}_slotp`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Control flow ---
|
||||
|
||||
if (op == "jump") {
|
||||
handlers.jump = function() {
|
||||
j_lbl = sanitize(a1)
|
||||
j_idx = label_pos[j_lbl]
|
||||
if (j_idx != null && j_idx < instr_idx) {
|
||||
@@ -2652,9 +2537,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jmp @${j_lbl}`)
|
||||
}
|
||||
last_was_term = true
|
||||
continue
|
||||
}
|
||||
if (op == "jump_true") {
|
||||
handlers.jump_true = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
jt_lbl = sanitize(a2)
|
||||
@@ -2669,9 +2553,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jmp @${jt_lbl}`)
|
||||
}
|
||||
emit(`@${p}_f`)
|
||||
continue
|
||||
}
|
||||
if (op == "jump_false") {
|
||||
handlers.jump_false = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
jf_lbl = sanitize(a2)
|
||||
@@ -2686,9 +2569,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jmp @${jf_lbl}`)
|
||||
}
|
||||
emit(`@${p}_t`)
|
||||
continue
|
||||
}
|
||||
if (op == "wary_true") {
|
||||
handlers.wary_true = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
wt_lbl = sanitize(a2)
|
||||
@@ -2703,9 +2585,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jmp @${wt_lbl}`)
|
||||
}
|
||||
emit(`@${p}_f`)
|
||||
continue
|
||||
}
|
||||
if (op == "wary_false") {
|
||||
handlers.wary_false = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
wf_lbl = sanitize(a2)
|
||||
@@ -2720,9 +2601,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jmp @${wf_lbl}`)
|
||||
}
|
||||
emit(`@${p}_t`)
|
||||
continue
|
||||
}
|
||||
if (op == "jump_null") {
|
||||
handlers.jump_null = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
jn_lbl = sanitize(a2)
|
||||
@@ -2737,9 +2617,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jnz %${p}, @${jn_lbl}, @${p}_n`)
|
||||
}
|
||||
emit(`@${p}_n`)
|
||||
continue
|
||||
}
|
||||
if (op == "jump_empty") {
|
||||
handlers.jump_empty = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
je_lbl = sanitize(a2)
|
||||
@@ -2754,9 +2633,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jnz %${p}, @${je_lbl}, @${p}_n`)
|
||||
}
|
||||
emit(`@${p}_n`)
|
||||
continue
|
||||
}
|
||||
if (op == "jump_not_null") {
|
||||
handlers.jump_not_null = function() {
|
||||
v = s_read(a1)
|
||||
p = fresh()
|
||||
jnn_lbl = sanitize(a2)
|
||||
@@ -2771,22 +2649,16 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jnz %${p}, @${jnn_lbl}, @${p}_n`)
|
||||
}
|
||||
emit(`@${p}_n`)
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Function calls [G] ---
|
||||
|
||||
if (op == "frame") {
|
||||
handlers.frame = function() {
|
||||
emit(` %fp =l call $__frame_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "apply") {
|
||||
handlers.apply = function() {
|
||||
emit(` %fp =l call $__apply_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "setarg") {
|
||||
handlers.setarg = function() {
|
||||
v = s_read(a1)
|
||||
lhs = s_read(a3)
|
||||
p = fresh()
|
||||
@@ -2795,9 +2667,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %${p}_fr =l and ${v}, -8`)
|
||||
emit(` %${p}_slot =l add %${p}_fr, ${text(32 + a2 * 8)}`)
|
||||
emit(` storel ${lhs}, %${p}_slot`)
|
||||
continue
|
||||
}
|
||||
if (op == "invoke" || op == "tail_invoke") {
|
||||
var invoke_handler = function() {
|
||||
// Signal dispatcher to call frame in slot a1 and resume at @_segN.
|
||||
seg_counter = seg_counter + 1
|
||||
resume_val = seg_counter * 65536 + a2
|
||||
@@ -2818,24 +2689,20 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` jnz %${p}_exc, @_exc_ret, @${p}_ok`)
|
||||
}
|
||||
emit(`@${p}_ok`)
|
||||
continue
|
||||
}
|
||||
if (op == "goframe") {
|
||||
handlers.invoke = invoke_handler
|
||||
handlers.tail_invoke = invoke_handler
|
||||
handlers.goframe = function() {
|
||||
emit(` %fp =l call $__goframe_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "goinvoke") {
|
||||
handlers.goinvoke = function() {
|
||||
// Tail call via dispatcher: no resume in this frame.
|
||||
emit(` call $cell_rt_signal_tail_call(l %ctx, l %fp, l ${text(a1)})`)
|
||||
emit(` ret ${text(qbe.js_null)}`)
|
||||
last_was_term = true
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Function object creation [G] ---
|
||||
|
||||
if (op == "function") {
|
||||
handlers["function"] = function() {
|
||||
fn_arity = 0
|
||||
fn_nr_slots = 0
|
||||
if (a2 >= 0 && a2 < length(ir.functions)) {
|
||||
@@ -2844,25 +2711,16 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
}
|
||||
emit(` %fp =l call $__function_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(fn_arity)}, l ${text(fn_nr_slots)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Record/Array creation [G] ---
|
||||
|
||||
if (op == "record") {
|
||||
handlers.record = function() {
|
||||
emit(` %fp =l call $__new_record_ss(l %ctx, l %fp, l ${text(a1)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
if (op == "array") {
|
||||
handlers.array = function() {
|
||||
emit(` %fp =l call $__new_array_ss(l %ctx, l %fp, l ${text(a1)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Array push/pop [G] ---
|
||||
|
||||
if (op == "push") {
|
||||
handlers.push = function() {
|
||||
lhs = s_read(a1)
|
||||
rhs = s_read(a2)
|
||||
p = fresh()
|
||||
@@ -2909,31 +2767,21 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %fp =l call $__push_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
emit_exc_check()
|
||||
emit(`@${p}_done`)
|
||||
continue
|
||||
}
|
||||
if (op == "pop") {
|
||||
handlers.pop = function() {
|
||||
emit(` %fp =l call $__pop_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Length [G] ---
|
||||
|
||||
if (op == "length") {
|
||||
handlers.length = function() {
|
||||
emit(` %fp =l call $__length_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Misc ---
|
||||
|
||||
if (op == "return") {
|
||||
handlers["return"] = function() {
|
||||
v = s_read(a1)
|
||||
emit(` ret ${v}`)
|
||||
last_was_term = true
|
||||
continue
|
||||
}
|
||||
if (op == "disrupt") {
|
||||
handlers.disrupt = function() {
|
||||
emit(` call $cell_rt_disrupt(l %ctx)`)
|
||||
if (has_handler && !in_handler) {
|
||||
emit(" jmp @disruption_handler")
|
||||
@@ -2941,9 +2789,8 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` ret 15`)
|
||||
}
|
||||
last_was_term = true
|
||||
continue
|
||||
}
|
||||
if (op == "delete") {
|
||||
handlers["delete"] = function() {
|
||||
pn = null
|
||||
if (is_text(a3)) {
|
||||
pn = a3
|
||||
@@ -2961,32 +2808,69 @@ var qbe_emit = function(ir, qbe, export_name) {
|
||||
emit(` %fp =l call $__delete_dynamic_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
}
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
|
||||
// --- in [G] ---
|
||||
|
||||
if (op == "in") {
|
||||
handlers["in"] = function() {
|
||||
// IR: ["in", dest, key_slot, obj_slot]
|
||||
emit(` %fp =l call $__in_ss(l %ctx, l %fp, l ${text(a1)}, l ${text(a2)}, l ${text(a3)})`)
|
||||
emit_exc_check()
|
||||
continue
|
||||
}
|
||||
|
||||
// --- regexp [G] ---
|
||||
|
||||
if (op == "regexp") {
|
||||
handlers.regexp = function() {
|
||||
// IR: ["regexp", dest_slot, pattern_string, flags_string]
|
||||
pat_label = intern_str(a2)
|
||||
flg_label = intern_str(a3)
|
||||
emit(` %fp =l call $__regexp_ss(l %ctx, l %fp, l ${text(a1)}, l ${pat_label.label}, l ${flg_label.label})`)
|
||||
emit_exc_check()
|
||||
}
|
||||
|
||||
// Walk instructions
|
||||
var last_was_term = false
|
||||
i = 0
|
||||
while (i < length(instrs)) {
|
||||
instr = instrs[i]
|
||||
instr_idx = i
|
||||
|
||||
// Emit @disruption_handler at the right flat index
|
||||
// disruption_pc counts all entries (labels + instructions)
|
||||
if (has_handler && i == disruption_pc) {
|
||||
if (!last_was_term) {
|
||||
emit(" jmp @disruption_handler")
|
||||
}
|
||||
emit("@disruption_handler")
|
||||
emit(" call $cell_rt_clear_exception(l %ctx)")
|
||||
emit(` %fp =l call $cell_rt_refresh_fp(l %ctx)`)
|
||||
last_was_term = false
|
||||
in_handler = true
|
||||
}
|
||||
i = i + 1
|
||||
|
||||
// Labels are plain strings; skip nop pseudo-labels from streamline
|
||||
if (is_text(instr)) {
|
||||
if (starts_with(instr, "_nop_")) continue
|
||||
lbl = sanitize(instr)
|
||||
if (!last_was_term) {
|
||||
emit(` jmp @${lbl}`)
|
||||
}
|
||||
emit("@" + lbl)
|
||||
last_was_term = false
|
||||
continue
|
||||
}
|
||||
|
||||
// --- Unknown opcode ---
|
||||
// Skip dead code: non-label instructions after a terminator are unreachable
|
||||
if (last_was_term) continue
|
||||
|
||||
op = instr[0]
|
||||
a1 = instr[1]
|
||||
a2 = instr[2]
|
||||
a3 = instr[3]
|
||||
last_was_term = false
|
||||
|
||||
handler = handlers[op]
|
||||
if (handler) {
|
||||
handler()
|
||||
} else {
|
||||
emit(` # unknown: ${op}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Emit @disrupt landing pad for arithmetic type-error branches
|
||||
if (!last_was_term) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
/* QBE headers */
|
||||
#include "all.h"
|
||||
@@ -154,6 +155,8 @@ JSValue js_os_qbe(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
}
|
||||
|
||||
/* Run the QBE pipeline */
|
||||
struct timespec t0, t1;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t0);
|
||||
parse(inf, "<ir>", qbe_dbgfile, qbe_data, qbe_func);
|
||||
fclose(inf);
|
||||
|
||||
@@ -162,6 +165,11 @@ JSValue js_os_qbe(JSContext *js, JSValue self, int argc, JSValue *argv) {
|
||||
fflush(qbe_outf);
|
||||
fclose(qbe_outf);
|
||||
qbe_outf = NULL;
|
||||
clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
double ms = (t1.tv_sec - t0.tv_sec) * 1000.0 +
|
||||
(t1.tv_nsec - t0.tv_nsec) / 1e6;
|
||||
fprintf(stderr, "[qbe_backend] os.qbe: %.1fms (%zu bytes IL -> %zu bytes asm)\n",
|
||||
ms, ir_len, out_len);
|
||||
|
||||
JS_FreeCString(js, ir);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user