const types = @import("types.zig");
const js = @import("js_helpers.zig");
const worker = @import("worker.zig");
const beam_proxy = @import("beam_proxy.zig");
const beam_helpers = @import("beam_helpers.zig");
const inspect_binary = beam_helpers.inspect_binary;
const term_to_binary = beam_helpers.term_to_binary;
const get_list_cell = beam_helpers.get_list_cell;
const map_iterator_create = beam_helpers.map_iterator_create;
const map_iterator_get_pair = beam_helpers.map_iterator_get_pair;
const std = types.std;
const e = types.e;
const qjs = types.qjs;
const MAX_DEPTH = 32;
pub fn convert(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm) qjs.JSValue {
return convert_recursive(ctx, env, term, 0);
}
fn convert_recursive(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm, depth: u32) qjs.JSValue {
if (depth > MAX_DEPTH) return js.js_null();
// Atom: nil, true, false, or atom string
var atom_buf: [256]u8 = undefined;
const atom_len = e.enif_get_atom(env, term, &atom_buf, atom_buf.len, e.ERL_NIF_LATIN1);
if (atom_len > 0) {
const name = atom_buf[0..@intCast(atom_len - 1)]; // exclude null terminator
const state: ?*worker.WorkerState = @ptrCast(@alignCast(qjs.JS_GetContextOpaque(ctx)));
if (state) |s| {
if (s.atoms.atomToJS(ctx, name)) |cached| return cached;
} else {
if (std.mem.eql(u8, name, "nil") or std.mem.eql(u8, name, "undefined")) return js.js_null();
if (std.mem.eql(u8, name, "true")) return js.js_true();
if (std.mem.eql(u8, name, "false")) return js.js_false();
}
return qjs.JS_NewStringLen(ctx, name.ptr, name.len);
}
// Integer
var i64_val: i64 = 0;
if (e.enif_get_int64(env, term, &i64_val) != 0) {
if (i64_val >= std.math.minInt(i32) and i64_val <= std.math.maxInt(i32)) {
return qjs.JS_NewInt32(ctx, @intCast(i64_val));
}
return qjs.JS_NewInt64(ctx, i64_val);
}
// Float
var f64_val: f64 = 0;
if (e.enif_get_double(env, term, &f64_val) != 0) {
return qjs.JS_NewFloat64(ctx, f64_val);
}
// Binary → string (UTF-8 text)
if (inspect_binary(env, term)) |bin| {
return qjs.JS_NewStringLen(ctx, bin.data, bin.size);
}
// List → Array
// List check: try to get list length
var list_len: c_uint = 0;
if (e.enif_get_list_length(env, term, &list_len) != 0) {
return convert_list(ctx, env, term, depth);
}
// Map → Object
if (e.enif_is_map(env, term) != 0) {
return convert_map(ctx, env, term, depth);
}
// PID → opaque JS object with ETF-encoded data
if (e.enif_is_pid(env, term) != 0) {
return make_beam_term(ctx, env, term, "pid");
}
// Reference → opaque JS object
if (e.enif_is_ref(env, term) != 0) {
return make_beam_term(ctx, env, term, "ref");
}
// Port → opaque JS object
if (e.enif_is_port(env, term) != 0) {
return make_beam_term(ctx, env, term, "port");
}
// Tuple
var tuple_arity: c_int = 0;
var tuple_elems: ?[*]const e.ErlNifTerm = null;
if (e.enif_get_tuple(env, term, &tuple_arity, @ptrCast(&tuple_elems)) != 0) {
const elems = tuple_elems orelse return js.js_null();
// {:bytes, binary} → Uint8Array
if (tuple_arity == 2) {
var tag_buf: [16]u8 = undefined;
const tag_len = e.enif_get_atom(env, elems[0], &tag_buf, tag_buf.len, e.ERL_NIF_LATIN1);
if (tag_len > 0 and std.mem.eql(u8, tag_buf[0..@intCast(tag_len - 1)], "bytes")) {
if (inspect_binary(env, elems[1])) |bbin| {
return make_uint8array(ctx, bbin.data, bbin.size);
}
}
}
// Generic tuple → Array
const arr = qjs.JS_NewArray(ctx);
for (0..@intCast(tuple_arity)) |idx| {
const elem = convert_recursive(ctx, env, elems[idx], depth + 1);
_ = qjs.JS_SetPropertyUint32(ctx, arr, @intCast(idx), elem);
}
return arr;
}
return js.js_null();
}
fn make_beam_term(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm, type_name: [*c]const u8) qjs.JSValue {
var bin = term_to_binary(env, term) orelse return js.js_null();
defer e.enif_release_binary(&bin);
const obj = qjs.JS_NewObject(ctx);
_ = qjs.JS_SetPropertyStr(ctx, obj, "__beam_type__", qjs.JS_NewStringLen(ctx, type_name, std.mem.len(type_name)));
_ = qjs.JS_SetPropertyStr(ctx, obj, "__beam_data__", make_uint8array(ctx, bin.data, bin.size));
return obj;
}
fn make_uint8array(ctx: *qjs.JSContext, data: [*c]u8, size: usize) qjs.JSValue {
const ab = qjs.JS_NewArrayBufferCopy(ctx, data, size);
if (js.js_is_exception(ab)) return js.js_exception();
const g = qjs.JS_GetGlobalObject(ctx);
defer qjs.JS_FreeValue(ctx, g);
const ctor = qjs.JS_GetPropertyStr(ctx, g, "Uint8Array");
defer qjs.JS_FreeValue(ctx, ctor);
var args = [_]qjs.JSValue{ab};
const result = qjs.JS_CallConstructor(ctx, ctor, 1, &args);
qjs.JS_FreeValue(ctx, ab);
return result;
}
fn convert_list(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm, depth: u32) qjs.JSValue {
// Check if it's an iolist-like structure (list of binaries/integers)
// For now, convert as a regular JS array
const arr = qjs.JS_NewArray(ctx);
var current = term;
var idx: u32 = 0;
while (true) {
const cell = get_list_cell(env, current) orelse break;
const elem = convert_recursive(ctx, env, cell.head, depth + 1);
_ = qjs.JS_SetPropertyUint32(ctx, arr, idx, elem);
idx += 1;
current = cell.tail;
}
return arr;
}
fn map_iterator_first() e.ErlNifMapIteratorEntry {
return switch (@typeInfo(e.ErlNifMapIteratorEntry)) {
.@"enum" => @as(e.ErlNifMapIteratorEntry, @enumFromInt(1)),
else => std.mem.zeroes(e.ErlNifMapIteratorEntry),
};
}
fn convert_map(ctx: *qjs.JSContext, env: ?*e.ErlNifEnv, term: e.ErlNifTerm, depth: u32) qjs.JSValue {
if (beam_proxy.class_id != 0) {
var map_size: usize = 0;
if (e.enif_get_map_size(env, term, &map_size) != 0 and map_size > 0) {
return beam_proxy.create(ctx, env, term);
}
}
const obj = qjs.JS_NewObject(ctx);
var iter = map_iterator_create(env, term, map_iterator_first()) orelse {
return obj;
};
defer e.enif_map_iterator_destroy(env, &iter);
while (map_iterator_get_pair(env, &iter)) |pair| {
var atom: qjs.JSAtom = qjs.JS_ATOM_NULL;
if (inspect_binary(env, pair.key)) |bin| {
if (bin.size > 0) {
atom = qjs.JS_NewAtomLen(ctx, bin.data, bin.size);
}
} else {
var atom_buf: [256]u8 = undefined;
const alen = e.enif_get_atom(env, pair.key, &atom_buf, atom_buf.len, e.ERL_NIF_LATIN1);
if (alen > 0) {
const name_len: usize = @intCast(alen - 1);
atom = qjs.JS_NewAtomLen(ctx, &atom_buf, name_len);
}
}
if (atom != qjs.JS_ATOM_NULL) {
const js_val = convert_recursive(ctx, env, pair.value, depth + 1);
_ = qjs.JS_SetProperty(ctx, obj, atom, js_val);
qjs.JS_FreeAtom(ctx, atom);
}
_ = e.enif_map_iterator_next(env, &iter);
}
return obj;
}