diff --git a/internal/engine.cm b/internal/engine.cm index b6a26349..73009edc 100644 --- a/internal/engine.cm +++ b/internal/engine.cm @@ -73,7 +73,21 @@ function ensure_build_dir() { return dir } -// Load a pipeline module from cache, with boot/ seed fallback +// Load a boot seed module (for compiling pipeline modules on cache miss) +function boot_load(name) { + var mcode_path = core_path + '/boot/' + name + '.cm.mcode' + var mcode_blob = null + var mach_blob = null + if (!fd.is_file(mcode_path)) { + print("error: missing boot seed: " + name + "\n") + disrupt + } + mcode_blob = fd.slurp(mcode_path) + mach_blob = mach_compile_mcode_bin(name, text(mcode_blob)) + return mach_load(mach_blob, {use: use_embed}) +} + +// Load a pipeline module from cache; on miss compile from source via boot chain function load_pipeline_module(name, env) { var source_path = core_path + '/' + name + '.cm' var source_blob = null @@ -82,14 +96,48 @@ function load_pipeline_module(name, env) { var mcode_path = null var mcode_blob = null var mach_blob = null + var src = null + var boot_tok = null + var boot_par = null + var boot_fld = null + var boot_mc = null + var tok_result = null + var ast = null + var compiled = null + var mcode_json = null if (fd.is_file(source_path)) { source_blob = fd.slurp(source_path) hash = content_hash(source_blob) cached = cache_path(hash) if (cached && fd.is_file(cached)) return mach_load(fd.slurp(cached), env) + + // Cache miss: compile from source using boot seed pipeline + mcode_path = core_path + '/boot/' + name + '.cm.mcode' + if (fd.is_file(mcode_path)) { + boot_tok = boot_load("tokenize") + boot_par = boot_load("parse") + boot_fld = boot_load("fold") + boot_mc = boot_load("mcode") + src = text(source_blob) + tok_result = boot_tok(src, source_path) + ast = boot_par(tok_result.tokens, src, source_path, boot_tok) + if (ast.errors != null && length(ast.errors) > 0) { + print("error: failed to compile pipeline module: " + name + "\n") + disrupt + } + ast = boot_fld(ast) + compiled = boot_mc(ast) + mcode_json = json.encode(compiled) + mach_blob = mach_compile_mcode_bin(name, mcode_json) + if (cached) { + ensure_build_dir() + fd.slurpwrite(cached, mach_blob) + } + return mach_load(mach_blob, env) + } } - // Boot seed fallback + // Last resort: boot seed as runtime (no source file found) mcode_path = core_path + '/boot/' + name + '.cm.mcode' if (fd.is_file(mcode_path)) { mcode_blob = fd.slurp(mcode_path) diff --git a/mcode.cm b/mcode.cm index 55377aa3..e259efc7 100644 --- a/mcode.cm +++ b/mcode.cm @@ -22,6 +22,12 @@ var mcode = function(ast) { "~!": "bitnot", "[]!": "load" } + var binop_sym = { + add: "+", subtract: "-", multiply: "*", divide: "/", + modulo: "%", pow: "**", + lt: "<", le: "<=", gt: ">", ge: ">=" + } + var compound_map = { "+=": "add", "-=": "subtract", "*=": "multiply", "/=": "divide", "%=": "modulo", "&=": "bitand", "|=": "bitor", "^=": "bitxor", @@ -67,6 +73,7 @@ var mcode = function(ast) { var _bp_right = 0 var _bp_ln = null var _bp_rn = null + var _bp_op_sym = null // State save/restore for nested function compilation var save_state = function() { @@ -240,6 +247,27 @@ var mcode = function(ast) { emit_1("null", dest) } + var emit_log_error = function(msg) { + var log_slot = alloc_slot() + add_instr(["access", log_slot, {kind: "name", name: "log", make: "intrinsic"}]) + var name_slot = alloc_slot() + emit_const_str(name_slot, "error") + var msg_slot = alloc_slot() + emit_const_str(msg_slot, msg) + var args_arr = alloc_slot() + add_instr(["array", args_arr, 0]) + emit_2("push", args_arr, msg_slot) + var result = alloc_slot() + var frame_slot = alloc_slot() + emit_3("frame", frame_slot, log_slot, 2) + var null_slot = alloc_slot() + emit_1("null", null_slot) + emit_3("setarg", frame_slot, 0, null_slot) + emit_3("setarg", frame_slot, 1, name_slot) + emit_3("setarg", frame_slot, 2, args_arr) + emit_2("invoke", frame_slot, result) + } + var emit_jump = function(label) { add_instr(["jump", label]) } @@ -321,6 +349,7 @@ var mcode = function(ast) { emit_jump(done) emit_label(err) + emit_log_error("cannot apply '+': operands must both be text or both be numbers") emit_0("disrupt") emit_label(done) return null @@ -345,6 +374,7 @@ var mcode = function(ast) { emit_jump(done) emit_label(err) + emit_log_error("cannot apply '" + _bp_op_sym + "': operands must be numbers") emit_0("disrupt") emit_label(done) return null @@ -569,6 +599,7 @@ var mcode = function(ast) { emit_jump(done) emit_label(err) + emit_log_error("cannot compare with '" + _bp_op_sym + "': operands must be same type") emit_0("disrupt") emit_label(done) return null @@ -589,6 +620,7 @@ var mcode = function(ast) { emit_jump(done) emit_label(err) + emit_log_error("cannot negate: operand must be a number") emit_0("disrupt") emit_label(done) return null @@ -607,6 +639,7 @@ var mcode = function(ast) { _bp_dest = dest _bp_left = left _bp_right = right + _bp_op_sym = binop_sym[op_str] || op_str if (op_str == "add") { emit_add_decomposed() } else if (op_str == "eq") { @@ -760,6 +793,7 @@ var mcode = function(ast) { // Error path: non-text key on function disrupts emit_label(error_path) + emit_log_error("cannot access: key must be text") emit_0("disrupt") emit_jump(done_label) @@ -1427,6 +1461,7 @@ var mcode = function(ast) { emit_2("push", arr_slot, val_slot) emit_jump(guard_done) emit_label(guard_err) + emit_log_error("cannot push: target must be an array") emit_0("disrupt") emit_label(guard_done) return val_slot @@ -1785,6 +1820,7 @@ var mcode = function(ast) { emit_2("push", a0, a1) emit_jump(guard_done) emit_label(guard_err) + emit_log_error("cannot push: target must be an array") emit_0("disrupt") emit_label(guard_done) return a1 @@ -2156,6 +2192,7 @@ var mcode = function(ast) { emit_2("pop", local_slot, arr_slot) emit_jump(guard_done) emit_label(guard_err) + emit_log_error("cannot pop: target must be an array") emit_0("disrupt") emit_label(guard_done) } diff --git a/parse.cm b/parse.cm index dc52a372..07c7f185 100644 --- a/parse.cm +++ b/parse.cm @@ -437,7 +437,14 @@ var parse = function(tokens, src, filename, tokenizer) { } push(list, pair) if (tok.kind == ",") advance() - else break + else if (tok.kind == "{") { + if (right && right.kind == "(") { + parse_error(tok, "unexpected '{' after property value; use method shorthand `name(args) { ... }` or `name: function(args) { ... }`") + } else { + parse_error(tok, "expected ',' or '}' in object literal") + } + break + } else break } ast_node_end(node) if (tok.kind == "}") advance() diff --git a/source/mach.c b/source/mach.c index ec8daa77..fca17c28 100644 --- a/source/mach.c +++ b/source/mach.c @@ -2124,41 +2124,45 @@ JSValue JS_CallRegisterVM(JSContext *ctx, JSCodeRegister *code, if (code->disruption_pc > 0 && frame_pc < code->disruption_pc) { env = fn->u.reg.env_record; pc = code->disruption_pc; + ctx->disruption_reported = FALSE; break; } if (JS_IsNull(frame->caller)) { - const char *fn_name = code->name_cstr ? code->name_cstr : ""; - const char *file = code->filename_cstr ? code->filename_cstr : ""; - uint16_t line = 0, col = 0; - if (code->line_table && frame_pc > 0 && frame_pc - 1 < code->instr_count) { - line = code->line_table[frame_pc - 1].line; - col = code->line_table[frame_pc - 1].col; - } - fprintf(stderr, "unhandled disruption in %s (%s:%u:%u)\n", fn_name, file, line, col); - /* Walk and print the frame chain as a stack trace */ - { - JSFrameRegister *trace_frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); - int first = 1; - while (trace_frame) { - if (!mist_is_function(trace_frame->function)) break; - JSFunction *trace_fn = JS_VALUE_GET_FUNCTION(trace_frame->function); - if (trace_fn->kind == JS_FUNC_KIND_REGISTER && trace_fn->u.reg.code) { - JSCodeRegister *tc = trace_fn->u.reg.code; - uint32_t tpc = first ? (frame_pc > 0 ? frame_pc - 1 : 0) - : (uint32_t)(JS_VALUE_GET_INT(trace_frame->address) >> 16); - uint16_t tl = 0, tcol = 0; - if (tc->line_table && tpc < tc->instr_count) { - tl = tc->line_table[tpc].line; - tcol = tc->line_table[tpc].col; - } - fprintf(stderr, " at %s (%s:%u:%u)\n", - tc->name_cstr ? tc->name_cstr : "", - tc->filename_cstr ? tc->filename_cstr : "", tl, tcol); - } - if (JS_IsNull(trace_frame->caller)) break; - trace_frame = (JSFrameRegister *)JS_VALUE_GET_PTR(trace_frame->caller); - first = 0; + if (!ctx->disruption_reported) { + const char *fn_name = code->name_cstr ? code->name_cstr : ""; + const char *file = code->filename_cstr ? code->filename_cstr : ""; + uint16_t line = 0, col = 0; + if (code->line_table && frame_pc > 0 && frame_pc - 1 < code->instr_count) { + line = code->line_table[frame_pc - 1].line; + col = code->line_table[frame_pc - 1].col; } + fprintf(stderr, "unhandled disruption in %s (%s:%u:%u)\n", fn_name, file, line, col); + /* Walk and print the frame chain as a stack trace */ + { + JSFrameRegister *trace_frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); + int first = 1; + while (trace_frame) { + if (!mist_is_function(trace_frame->function)) break; + JSFunction *trace_fn = JS_VALUE_GET_FUNCTION(trace_frame->function); + if (trace_fn->kind == JS_FUNC_KIND_REGISTER && trace_fn->u.reg.code) { + JSCodeRegister *tc = trace_fn->u.reg.code; + uint32_t tpc = first ? (frame_pc > 0 ? frame_pc - 1 : 0) + : (uint32_t)(JS_VALUE_GET_INT(trace_frame->address) >> 16); + uint16_t tl = 0, tcol = 0; + if (tc->line_table && tpc < tc->instr_count) { + tl = tc->line_table[tpc].line; + tcol = tc->line_table[tpc].col; + } + fprintf(stderr, " at %s (%s:%u:%u)\n", + tc->name_cstr ? tc->name_cstr : "", + tc->filename_cstr ? tc->filename_cstr : "", tl, tcol); + } + if (JS_IsNull(trace_frame->caller)) break; + trace_frame = (JSFrameRegister *)JS_VALUE_GET_PTR(trace_frame->caller); + first = 0; + } + } + ctx->disruption_reported = TRUE; } result = JS_Throw(ctx, JS_NULL); frame = (JSFrameRegister *)JS_VALUE_GET_PTR(frame_ref.val); diff --git a/source/quickjs-internal.h b/source/quickjs-internal.h index c2a2f5b9..db62d759 100644 --- a/source/quickjs-internal.h +++ b/source/quickjs-internal.h @@ -1147,6 +1147,8 @@ struct JSContext { JSValue current_exception; + JS_BOOL disruption_reported; + /* Actor identity key — used by wota/nota PRIVATE serialization */ JSValue actor_sym; diff --git a/tests/errors.cm b/tests/errors.cm new file mode 100644 index 00000000..9cb682b1 --- /dev/null +++ b/tests/errors.cm @@ -0,0 +1,153 @@ +// Runtime type error tests — verify each type mismatch disrupts correctly +return { + test_text_plus_array_disrupts: function() { + var caught = false + var fn = function() { + var x = "hello" + [1, 2, 3] + } disruption { + caught = true + } + fn() + if (!caught) return "text + array should disrupt" + }, + + test_number_plus_text_disrupts: function() { + var caught = false + var fn = function() { + var x = 1 + "hello" + } disruption { + caught = true + } + fn() + if (!caught) return "number + text should disrupt" + }, + + test_number_plus_array_disrupts: function() { + var caught = false + var fn = function() { + var x = 1 + [1, 2] + } disruption { + caught = true + } + fn() + if (!caught) return "number + array should disrupt" + }, + + test_text_multiply_disrupts: function() { + var caught = false + var fn = function() { + var x = "hello" * 2 + } disruption { + caught = true + } + fn() + if (!caught) return "text * number should disrupt" + }, + + test_text_divide_disrupts: function() { + var caught = false + var fn = function() { + var x = "hello" / 2 + } disruption { + caught = true + } + fn() + if (!caught) return "text / number should disrupt" + }, + + test_text_modulo_disrupts: function() { + var caught = false + var fn = function() { + var x = "hello" % 2 + } disruption { + caught = true + } + fn() + if (!caught) return "text % number should disrupt" + }, + + test_array_subtract_disrupts: function() { + var caught = false + var fn = function() { + var x = [1] - 2 + } disruption { + caught = true + } + fn() + if (!caught) return "array - number should disrupt" + }, + + test_negate_text_disrupts: function() { + var caught = false + var s = "hello" + var fn = function() { + return -s + } disruption { + caught = true + } + fn() + if (!caught) return "negate text should disrupt" + }, + + test_push_on_non_array_disrupts: function() { + var caught = false + var fn = function() { + var x = "hello" + x[] = 1 + } disruption { + caught = true + } + fn() + if (!caught) return "push on non-array should disrupt" + }, + + test_pop_on_non_array_disrupts: function() { + var caught = false + var s = "hello" + var fn = function() { + var v = s[] + return v + } disruption { + caught = true + } + fn() + if (!caught) return "pop on non-array should disrupt" + }, + + test_comparison_type_mismatch_disrupts: function() { + var caught = false + var fn = function() { + var x = "hello" < [1] + } disruption { + caught = true + } + fn() + if (!caught) return "text < array should disrupt" + }, + + test_explicit_disrupt_works: function() { + var caught = false + var fn = function() { + disrupt + } disruption { + caught = true + } + fn() + if (!caught) return "explicit disrupt should be caught" + }, + + test_valid_add_text: function() { + var x = "hello" + " world" + if (x != "hello world") return "text + text should work" + }, + + test_valid_add_numbers: function() { + var x = 1 + 2 + if (x != 3) return "number + number should work" + }, + + test_valid_comparison: function() { + if (!(1 < 2)) return "1 < 2 should be true" + if (!("a" < "b")) return "a < b should be true" + } +}