diff --git a/lang/src/mir/min_emitter.hako b/lang/src/mir/min_emitter.hako index 8b0653c5..2ef52e22 100644 --- a/lang/src/mir/min_emitter.hako +++ b/lang/src/mir/min_emitter.hako @@ -111,4 +111,8 @@ static box MinMirEmitter { } } -static box MinMirEmitterMain { main(args){ return 0 } } +using "lang/src/shared/common/entry_point_base.hako" as EntryPointBaseBox + +static box MinMirEmitterMain { + main(args) { return EntryPointBaseBox.main(args) } +} diff --git a/lang/src/shared/common/common_imports.hako b/lang/src/shared/common/common_imports.hako new file mode 100644 index 00000000..b9396ff3 --- /dev/null +++ b/lang/src/shared/common/common_imports.hako @@ -0,0 +1,18 @@ +// CommonImportsBox - Unified import utilities for string operations +// Consolidates frequently used StringHelpers and StringOps imports + +using "lang/src/shared/common/string_helpers.hako" as StringHelpers +using selfhost.shared.common.string_ops as StringOps + +static box CommonImportsBox { + // Provides access to common string utilities + // Boxes can import this instead of individual modules + static helpers() { return StringHelpers } + static ops() { return StringOps } + + // Commonly used string operations + static read_digits(text, pos) { return StringHelpers.read_digits(text, pos) } + static to_i64(digits) { return StringHelpers.to_i64(digits) } + static index_of_from(text, pattern, start) { return StringOps.index_of_from(text, pattern, start) } + static substring(text, start, end) { return StringOps.substring(text, start, end) } +} diff --git a/lang/src/shared/common/entry_point_base.hako b/lang/src/shared/common/entry_point_base.hako new file mode 100644 index 00000000..9715a7d8 --- /dev/null +++ b/lang/src/shared/common/entry_point_base.hako @@ -0,0 +1,18 @@ +// EntryPointBaseBox - Common entry point for standalone boxes +// Eliminates repetitive main(args){ return 0 } boilerplate + +static box EntryPointBaseBox { + // Standard entry point implementation + // Can be overridden by specific boxes if needed + static main(args) { + return 0 + } + + // Entry point with validation + static safe_main(args) { + if args == null { + return 1 + } + return EntryPointBaseBox.main(args) + } +} diff --git a/lang/src/shared/mir/mir_io_box.hako b/lang/src/shared/mir/mir_io_box.hako index 736cca10..dfdd9c4e 100644 --- a/lang/src/shared/mir/mir_io_box.hako +++ b/lang/src/shared/mir/mir_io_box.hako @@ -16,7 +16,7 @@ using "lang/src/vm/hakorune-vm/blocks_locator.hako" as BlocksLocatorBox using "lang/src/vm/hakorune-vm/instrs_locator.hako" as InstrsLocatorBox using "lang/src/vm/hakorune-vm/backward_object_scanner.hako" as BackwardObjectScannerBox using "lang/src/vm/hakorune-vm/block_iterator.hako" as BlockIteratorBox -using selfhost.shared.common.string_helpers as StringHelpers +using "lang/src/shared/common/common_imports.hako" as CommonImports using selfhost.shared.common.box_helpers as BoxHelpers static box MirIoBox { @@ -144,7 +144,7 @@ static box MirIoBox { p = p + key_id.length() // skip ws loop(p < obj.length()) { local ch = obj.substring(p,p+1) if ch == " " || ch == "\n" || ch == "\r" || ch == "\t" { p = p + 1 continue } break } - local digs = StringHelpers.read_digits(obj, p) + local digs = CommonImports.read_digits(obj, p) if digs == "" { return Result.Err("invalid block id") } ids.set(StringHelpers.int_to_str(StringHelpers.to_i64(digs)), 1) // require terminator diff --git a/lang/src/vm/hakorune-vm/core_bridge_ops.hako b/lang/src/vm/hakorune-vm/core_bridge_ops.hako index c0d481e7..d2be0188 100644 --- a/lang/src/vm/hakorune-vm/core_bridge_ops.hako +++ b/lang/src/vm/hakorune-vm/core_bridge_ops.hako @@ -4,6 +4,7 @@ using "lang/src/vm/boxes/result_box.hako" as Result using "lang/src/vm/hakorune-vm/json_field_extractor.hako" as JsonFieldExtractor +using "lang/src/vm/hakorune-vm/inst_field_extractor.hako" as InstFieldExtractor using "lang/src/vm/hakorune-vm/value_manager.hako" as ValueManagerBox using "lang/src/vm/hakorune-vm/args_extractor.hako" as ArgsExtractorBox using "lang/src/shared/common/string_helpers.hako" as StringHelpers @@ -36,8 +37,8 @@ static box CoreBridgeOps { local st = NyVmState.birth() local rc = NyVmOpConst.handle(inst_json, st) if rc < 0 { return Result.Err("const: core failure") } - // Reflect dst into hakorune-vm regs - local dst = JsonFieldExtractor.extract_int(inst_json, "dst") + // Reflect dst into hakorune-vm regs using unified extractor + local dst = InstFieldExtractor.extract_dst(inst_json) if dst == null { return Result.Err("const: dst not found") } local v = NyVmState.get_reg(st, dst) ValueManagerBox.set(regs, dst, v) @@ -46,40 +47,30 @@ static box CoreBridgeOps { // Apply binop via Core apply_binop(inst_json, regs) { - // Extract required fields - local dst = JsonFieldExtractor.extract_int(inst_json, "dst") + // Extract required fields using unified extractor + local dst = InstFieldExtractor.extract_dst(inst_json) if dst == null { return Result.Err("binop: dst field not found") } - local lhs = JsonFieldExtractor.extract_int(inst_json, "lhs") - if lhs == null { return Result.Err("binop: lhs field not found") } - local rhs = JsonFieldExtractor.extract_int(inst_json, "rhs") - if rhs == null { return Result.Err("binop: rhs field not found") } + local binary_ops = InstFieldExtractor.extract_binary_ops(inst_json) + if binary_ops.lhs == null { return Result.Err("binop: lhs field not found") } + if binary_ops.rhs == null { return Result.Err("binop: rhs field not found") } - // Normalize operation: prefer symbolic 'operation', else map from 'op_kind' - local op = JsonFieldExtractor.extract_string(inst_json, "operation") - if op == null { - local kind = JsonFieldExtractor.extract_string(inst_json, "op_kind") - if kind == null { return Result.Err("binop: operation/op_kind not found") } - if kind == "Add" { op = "+" } - else if kind == "Sub" { op = "-" } - else if kind == "Mul" { op = "*" } - else if kind == "Div" { op = "/" } - else if kind == "Mod" { op = "%" } - else { return Result.Err("binop: unsupported op_kind: " + kind) } - } + // Use unified binary ops extractor (includes operation normalization) + local op = binary_ops.operation + if op == null { return Result.Err("binop: operation/op_kind not found") } // Guard: required src regs must be set - local lhs_val = ValueManagerBox.get(regs, lhs) - if lhs_val == null { return Result.Err("binop: lhs v%" + lhs + " is unset") } - local rhs_val = ValueManagerBox.get(regs, rhs) - if rhs_val == null { return Result.Err("binop: rhs v%" + rhs + " is unset") } + local lhs_val = ValueManagerBox.get(regs, binary_ops.lhs) + if lhs_val == null { return Result.Err("binop: lhs v%" + binary_ops.lhs + " is unset") } + local rhs_val = ValueManagerBox.get(regs, binary_ops.rhs) + if rhs_val == null { return Result.Err("binop: rhs v%" + binary_ops.rhs + " is unset") } // Rebuild minimal JSON acceptable to Core - local j = "{\"op\":\"binop\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}" + local j = "{\"op\":\"binop\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + binary_ops.lhs + ",\"rhs\":" + binary_ops.rhs + "}" // Execute via Core with a temporary state containing sources local st = NyVmState.birth() - NyVmState.set_reg(st, lhs, lhs_val) - NyVmState.set_reg(st, rhs, rhs_val) + NyVmState.set_reg(st, binary_ops.lhs, lhs_val) + NyVmState.set_reg(st, binary_ops.rhs, rhs_val) local rc = NyVmOpBinOp.handle(j, st) if rc < 0 { return Result.Err("binop: core failure") } local out = NyVmState.get_reg(st, dst) @@ -101,35 +92,24 @@ static box CoreBridgeOps { // Apply compare via Core apply_compare(inst_json, regs) { - local dst = JsonFieldExtractor.extract_int(inst_json, "dst") + local dst = InstFieldExtractor.extract_dst(inst_json) if dst == null { return Result.Err("compare: dst field not found") } - local lhs = JsonFieldExtractor.extract_int(inst_json, "lhs") - if lhs == null { return Result.Err("compare: lhs field not found") } - local rhs = JsonFieldExtractor.extract_int(inst_json, "rhs") - if rhs == null { return Result.Err("compare: rhs field not found") } - // Normalize kind -> operation - local op = JsonFieldExtractor.extract_string(inst_json, "operation") - if op == null { - local kind = JsonFieldExtractor.extract_string(inst_json, "kind") - if kind == null { return Result.Err("compare: kind/operation not found") } - if kind == "Eq" { op = "==" } - else if kind == "Ne" { op = "!=" } - else if kind == "Lt" { op = "<" } - else if kind == "Le" { op = "<=" } - else if kind == "Gt" { op = ">" } - else if kind == "Ge" { op = ">=" } - else { return Result.Err("compare: unsupported kind: " + kind) } - } + local compare_ops = InstFieldExtractor.extract_compare_ops(inst_json) + if compare_ops.lhs == null { return Result.Err("compare: lhs field not found") } + if compare_ops.rhs == null { return Result.Err("compare: rhs field not found") } + // Use unified compare ops extractor (includes operation normalization) + local op = compare_ops.operation + if op == null { return Result.Err("compare: kind/operation not found") } // Guards - local lv = ValueManagerBox.get(regs, lhs) - if lv == null { return Result.Err("compare: lhs v%" + lhs + " is unset") } - local rv = ValueManagerBox.get(regs, rhs) - if rv == null { return Result.Err("compare: rhs v%" + rhs + " is unset") } + local lv = ValueManagerBox.get(regs, compare_ops.lhs) + if lv == null { return Result.Err("compare: lhs v%" + compare_ops.lhs + " is unset") } + local rv = ValueManagerBox.get(regs, compare_ops.rhs) + if rv == null { return Result.Err("compare: rhs v%" + compare_ops.rhs + " is unset") } // Build minimal JSON - local j = "{\"op\":\"compare\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}" + local j = "{\"op\":\"compare\",\"dst\":" + dst + ",\"operation\":\"" + op + "\",\"lhs\":" + compare_ops.lhs + ",\"rhs\":" + compare_ops.rhs + "}" local st = NyVmState.birth() - NyVmState.set_reg(st, lhs, lv) - NyVmState.set_reg(st, rhs, rv) + NyVmState.set_reg(st, compare_ops.lhs, lv) + NyVmState.set_reg(st, compare_ops.rhs, rv) local rc = NyVmOpCompare.handle(j, st) if rc < 0 { return Result.Err("compare: core failure") } ValueManagerBox.set(regs, dst, NyVmState.get_reg(st, dst)) diff --git a/lang/src/vm/hakorune-vm/inst_field_extractor.hako b/lang/src/vm/hakorune-vm/inst_field_extractor.hako index 8d5444f5..c7f85d92 100644 --- a/lang/src/vm/hakorune-vm/inst_field_extractor.hako +++ b/lang/src/vm/hakorune-vm/inst_field_extractor.hako @@ -23,12 +23,24 @@ static box InstFieldExtractorBox { return result } - // Extract comparison fields (lhs, rhs, kind) + // Extract comparison fields (lhs, rhs, kind) and normalize to operation static extract_compare_ops(inst_json) { local result = {} result.lhs = JsonFieldExtractor.extract_int(inst_json, "lhs") result.rhs = JsonFieldExtractor.extract_int(inst_json, "rhs") result.kind = JsonFieldExtractor.extract_string(inst_json, "kind") + + // Normalize kind -> operation + local op = JsonFieldExtractor.extract_string(inst_json, "operation") + if op == null && result.kind != null { + if result.kind == "Eq" { op = "==" } + else if result.kind == "Ne" { op = "!=" } + else if result.kind == "Lt" { op = "<" } + else if result.kind == "Le" { op = "<=" } + else if result.kind == "Gt" { op = ">" } + else if result.kind == "Ge" { op = ">=" } + } + result.operation = op return result } diff --git a/src/backend/mir_interpreter/handlers/boxes_map.rs b/src/backend/mir_interpreter/handlers/boxes_map.rs index 2557a0fb..9f696429 100644 --- a/src/backend/mir_interpreter/handlers/boxes_map.rs +++ b/src/backend/mir_interpreter/handlers/boxes_map.rs @@ -64,7 +64,12 @@ pub(super) fn try_handle_map_box( } "set" => { if args.len() != 2 { return Err(VMError::InvalidInstruction("MapBox.set expects 2 args".into())); } - let k = this.reg_load(args[0])?.to_nyash_box(); + let k_vm = this.reg_load(args[0])?; + if !matches!(k_vm, VMValue::String(_)) { + if let Some(d) = dst { this.regs.insert(d, VMValue::String("[map/bad-key] key must be string".to_string())); } + return Ok(true); + } + let k = k_vm.to_nyash_box(); let v = this.reg_load(args[1])?.to_nyash_box(); let ret = mb.set(k, v); if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); } @@ -72,7 +77,12 @@ pub(super) fn try_handle_map_box( } "get" => { if args.len() != 1 { return Err(VMError::InvalidInstruction("MapBox.get expects 1 arg".into())); } - let k = this.reg_load(args[0])?.to_nyash_box(); + let k_vm = this.reg_load(args[0])?; + if !matches!(k_vm, VMValue::String(_)) { + if let Some(d) = dst { this.regs.insert(d, VMValue::String("[map/bad-key] key must be string".to_string())); } + return Ok(true); + } + let k = k_vm.to_nyash_box(); let ret = mb.get(k); if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); } return Ok(true); @@ -86,7 +96,12 @@ pub(super) fn try_handle_map_box( } "delete" => { if args.len() != 1 { return Err(VMError::InvalidInstruction("MapBox.delete expects 1 arg".into())); } - let k = this.reg_load(args[0])?.to_nyash_box(); + let k_vm = this.reg_load(args[0])?; + if !matches!(k_vm, VMValue::String(_)) { + if let Some(d) = dst { this.regs.insert(d, VMValue::String("[map/bad-key] key must be string".to_string())); } + return Ok(true); + } + let k = k_vm.to_nyash_box(); let ret = mb.delete(k); if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); } return Ok(true); diff --git a/src/runner/json_v1_bridge.rs b/src/runner/json_v1_bridge.rs index 990796b8..9101a6f7 100644 --- a/src/runner/json_v1_bridge.rs +++ b/src/runner/json_v1_bridge.rs @@ -467,6 +467,16 @@ pub fn try_parse_v1_to_module(json: &str) -> Result, String> { "mir_call callee Closure missing func in function '{}'", func_name ))? as u32; + // If captures exist, append them to argv (best-effort minimal semantics) + if let Some(caps) = callee_obj.get("captures").and_then(Value::as_array) { + for c in caps { + let id = c.as_u64().ok_or_else(|| format!( + "mir_call Closure capture must be integer in function '{}'", + func_name + ))? as u32; + argv.push(ValueId::new(id)); + } + } // Captures (if any) are currently ignored at this stage; captured values are // expected to be materialized as arguments or handled by earlier lowering. block_ref.add_instruction(MirInstruction::Call { diff --git a/tools/smokes/v2/profiles/quick/core/bridge/canonicalize_noop_method_on_vm.sh b/tools/smokes/v2/profiles/quick/core/bridge/canonicalize_noop_method_on_vm.sh new file mode 100644 index 00000000..3ef680f5 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/bridge/canonicalize_noop_method_on_vm.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# canonicalize_noop_method_on_vm.sh — ONでもMethodは変異しない(dump-mut未生成) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then + ROOT="$ROOT_GIT" +else + ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)" +fi +source "$ROOT/tools/smokes/v2/lib/test_runner.sh" +require_env || exit 2 + +json_path="/tmp/ny_v1_noop_method_$$.json" +mut_on="/tmp/ny_v1_noop_method_on_$$.json" + +cat >"$json_path" <<'JSON' +{"schema_version":"1.0","functions":[{"name":"main","blocks":[{"id":0,"instructions":[{"op":"mir_call","mir_call":{"callee":{"type":"Method","box_name":"ArrayBox","method":"size","receiver":1},"args":[] }},{"op":"ret"}]}]}]} +JSON + +set +e +HAKO_NYVM_V1_DOWNCONVERT=1 HAKO_BRIDGE_INJECT_SINGLETON=1 HAKO_DEBUG_NYVM_BRIDGE_DUMP_MUT="$mut_on" \ + "$ROOT/target/release/nyash" --json-file "$json_path" >/dev/null 2>&1 +set -e || true + +if [ -f "$mut_on" ]; then + echo "[FAIL] canonicalize_noop_method_on_vm: mutated dump should not be created for Method" >&2 + exit 1 +fi + +echo "[PASS] canonicalize_noop_method_on_vm" +rm -f "$json_path" "$mut_on" +exit 0 + diff --git a/tools/smokes/v2/profiles/quick/core/gate_c_invalid_header_vm.sh b/tools/smokes/v2/profiles/quick/core/gate_c_invalid_header_vm.sh new file mode 100644 index 00000000..f312ef2a --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/gate_c_invalid_header_vm.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# gate_c_invalid_header_vm.sh — Gate‑C(Core) invalid JSON header → 非0終了 + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then + ROOT="$ROOT_GIT" +else + ROOT="$(cd "$SCRIPT_DIR/../../../../../../.." && pwd)" +fi +source "$ROOT/tools/smokes/v2/lib/test_runner.sh" +require_env || exit 2 + +bad="/tmp/gatec_bad_$$.json" +echo '{"bad":1}' > "$bad" +set +e +NYASH_GATE_C_CORE=1 "$NYASH_BIN" --nyvm-json-file "$bad" >/dev/null 2>&1 +rc=$? +set -e +rm -f "$bad" +if [ $rc -ne 0 ]; then + echo "[PASS] gate_c_invalid_header_vm" +else + echo "[FAIL] gate_c_invalid_header_vm (rc=$rc)" >&2 + exit 1 +fi + diff --git a/tools/smokes/v2/profiles/quick/core/map/map_bad_key_delete_vm.sh b/tools/smokes/v2/profiles/quick/core/map/map_bad_key_delete_vm.sh new file mode 100644 index 00000000..284c2ae6 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/map/map_bad_key_delete_vm.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# map_bad_key_delete_vm.sh — Map.delete with non-string key returns [map/bad-key] + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then + ROOT="$ROOT_GIT" +else + ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)" +fi +source "$ROOT/tools/smokes/v2/lib/test_runner.sh" +require_env || exit 2 + +code='static box Main { main() { local m=new MapBox(); print(m.delete(123)); return 0 } }' +out=$(run_nyash_vm -c "$code") +if echo "$out" | grep -q "\[map/bad-key\]"; then + echo "[PASS] map_bad_key_delete_vm" +else + echo "[FAIL] map_bad_key_delete_vm" >&2; echo "$out" >&2; exit 1 +fi + diff --git a/tools/smokes/v2/profiles/quick/core/map/map_bad_key_get_vm.sh b/tools/smokes/v2/profiles/quick/core/map/map_bad_key_get_vm.sh new file mode 100644 index 00000000..c24e8c39 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/map/map_bad_key_get_vm.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# map_bad_key_get_vm.sh — Map.get with non-string key returns [map/bad-key] + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then + ROOT="$ROOT_GIT" +else + ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)" +fi +source "$ROOT/tools/smokes/v2/lib/test_runner.sh" +require_env || exit 2 + +code='static box Main { main() { local m=new MapBox(); print(m.get(123)); return 0 } }' +out=$(run_nyash_vm -c "$code") +if echo "$out" | grep -q "\[map/bad-key\]"; then + echo "[PASS] map_bad_key_get_vm" +else + echo "[FAIL] map_bad_key_get_vm" >&2; echo "$out" >&2; exit 1 +fi + diff --git a/tools/smokes/v2/profiles/quick/core/map/map_bad_key_set_vm.sh b/tools/smokes/v2/profiles/quick/core/map/map_bad_key_set_vm.sh new file mode 100644 index 00000000..b3e5cee8 --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/map/map_bad_key_set_vm.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# map_bad_key_set_vm.sh — Map.set with non-string key returns [map/bad-key] + +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then + ROOT="$ROOT_GIT" +else + ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)" +fi +source "$ROOT/tools/smokes/v2/lib/test_runner.sh" +require_env || exit 2 + +code='static box Main { main() { local m=new MapBox(); print(m.set(123, 1)); return 0 } }' +out=$(run_nyash_vm -c "$code") +if echo "$out" | grep -q "\[map/bad-key\]"; then + echo "[PASS] map_bad_key_set_vm" +else + echo "[FAIL] map_bad_key_set_vm" >&2; echo "$out" >&2; exit 1 +fi + diff --git a/tools/smokes/v2/profiles/quick/core/string/substring_clamp_vm.sh b/tools/smokes/v2/profiles/quick/core/string/substring_clamp_vm.sh new file mode 100644 index 00000000..f309244d --- /dev/null +++ b/tools/smokes/v2/profiles/quick/core/string/substring_clamp_vm.sh @@ -0,0 +1,34 @@ +#!/bin/bash +# substring_clamp_vm.sh — String.substring clamps to [0,size] and start<=end + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +if ROOT_GIT=$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null); then + ROOT="$ROOT_GIT" +else + ROOT="$(cd "$SCRIPT_DIR/../../../../../../../../.." && pwd)" +fi +source "$ROOT/tools/smokes/v2/lib/test_runner.sh" +require_env || exit 2 + +code='static box Main { main() { + local s="abcd"; + print(s.substring(-5, 2)); + print(s.substring(2, 99)); + print(s.substring(3, 1)); + return 0 +} }' +out=$(run_nyash_vm -c "$code") +expected=$(cat <&2; printf '--- expected ---\n%s--- got ---\n%s\n' "$expected" "$out" >&2; exit 1 +fi +