🔧 Fix peek expression terminator issues and add ternary operator support
- Fix LLVM IR terminator missing in peek expression entry blocks - Add proper jump instructions between peek blocks - Implement ternary operator (? :) as syntactic sugar for peek - Update Python LLVM externcall handling for improved compatibility - Add comprehensive test cases for peek and ternary expressions - Update language guide with ternary operator documentation ChatGPTが頑張って修正してくれたにゃ!🐱 Co-Authored-By: ChatGPT <noreply@openai.com>
This commit is contained in:
@ -1,5 +1,23 @@
|
|||||||
# Current Task (2025-09-14 改定) — Phase 15 llvmlite(既定)+ PyVM(新規)
|
# Current Task (2025-09-14 改定) — Phase 15 llvmlite(既定)+ PyVM(新規)
|
||||||
|
|
||||||
|
Context Snapshot — Open After Reset
|
||||||
|
- Status: A6 受入(PyVM↔llvmlite parity + LLVM verify→.o→EXE)完了。
|
||||||
|
- Lang: peek ブロック式(最後の式が値)OK、式文フォールバックOK、三項(?:)パーサ導入済み(VM E2E 緑)。
|
||||||
|
- Docs: 言語/アーキの入口を整備(guides/language-guide.md, reference/language/**, reference/architecture/**)。
|
||||||
|
- Parser MVP: Python Stage‑1 実装 + roundtrip スモーク緑。Nyash 実装スケルトン配置済み。
|
||||||
|
|
||||||
|
Next (short)
|
||||||
|
1) 三項(?:)の PyVM/llvmlite パリティE2E(`tools/parity.sh`)
|
||||||
|
2) peek サンプル/テスト拡充(式文・ブロック・戻り値)
|
||||||
|
3) Docs 追補(最小例、must_use 注意)
|
||||||
|
4) Parser MVP Stage‑2 設計(local/if/loop/call/method/new/me/substring/length/lastIndexOf)
|
||||||
|
|
||||||
|
Quick Verify
|
||||||
|
- Ternary (VM): create file with `return (1 < 2) ? 10 : 20` and run
|
||||||
|
`./target/release/nyash --backend vm /tmp/tern4.nyash`
|
||||||
|
- Peek block (VM): `./target/release/nyash --backend vm apps/tests/peek_expr_block.nyash`
|
||||||
|
- Python parser RT: `NYASH_CLI_VERBOSE=1 tools/ny_parser_mvp_roundtrip.sh`
|
||||||
|
|
||||||
Summary
|
Summary
|
||||||
- JIT/Cranelift は一時停止。Rust/inkwell LLVM は参照のみ。
|
- JIT/Cranelift は一時停止。Rust/inkwell LLVM は参照のみ。
|
||||||
- 既定の実行/ビルド経路は Python/llvmlite ハーネス(MIR JSON→.o→NyRT link)。
|
- 既定の実行/ビルド経路は Python/llvmlite ハーネス(MIR JSON→.o→NyRT link)。
|
||||||
|
|||||||
Binary file not shown.
BIN
app_parity_main
BIN
app_parity_main
Binary file not shown.
BIN
app_parity_peek_expr_block
Normal file
BIN
app_parity_peek_expr_block
Normal file
Binary file not shown.
BIN
app_parity_peek_return_value
Normal file
BIN
app_parity_peek_return_value
Normal file
Binary file not shown.
BIN
app_parity_ternary_basic
Normal file
BIN
app_parity_ternary_basic
Normal file
Binary file not shown.
BIN
app_parity_ternary_nested
Normal file
BIN
app_parity_ternary_nested
Normal file
Binary file not shown.
BIN
app_peek_expr_block
Normal file
BIN
app_peek_expr_block
Normal file
Binary file not shown.
15
apps/tests/peek_return_value.nyash
Normal file
15
apps/tests/peek_return_value.nyash
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
static box Main {
|
||||||
|
main(args) {
|
||||||
|
// Use peek as an expression to produce a value
|
||||||
|
local d = "1"
|
||||||
|
local v = peek d {
|
||||||
|
"0" => { 0 }
|
||||||
|
"1" => { 1 }
|
||||||
|
else => { 0 }
|
||||||
|
}
|
||||||
|
// Show the result and finish
|
||||||
|
local console = new ConsoleBox()
|
||||||
|
console.println(v)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/tests/ternary_nested.nyash
Normal file
10
apps/tests/ternary_nested.nyash
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
static box Main {
|
||||||
|
main(args) {
|
||||||
|
local a = 3
|
||||||
|
local b = 5
|
||||||
|
// Nested ternary: should evaluate to 50
|
||||||
|
local v = (a < b) ? ((b < 0) ? 40 : 50) : 60
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -15,6 +15,39 @@ Common Constructs
|
|||||||
- Null-coalesce: `x ?? y` → `peek x { null => y, else => x }`
|
- Null-coalesce: `x ?? y` → `peek x { null => y, else => x }`
|
||||||
- Safe access: `a?.b` → `peek a { null => null, else => a.b }`
|
- Safe access: `a?.b` → `peek a { null => null, else => a.b }`
|
||||||
|
|
||||||
|
Minimal Examples
|
||||||
|
- Ternary
|
||||||
|
```nyash
|
||||||
|
static box Main {
|
||||||
|
main(args) {
|
||||||
|
local a = 3
|
||||||
|
local b = 5
|
||||||
|
// Nested ternary is supported
|
||||||
|
local v = (a < b) ? ((b < 0) ? 40 : 50) : 60
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Peek as expression block (last expression is the value)
|
||||||
|
```nyash
|
||||||
|
static box Main {
|
||||||
|
main(args) {
|
||||||
|
local d = "1"
|
||||||
|
// Each arm can be a block; the last expression becomes the value
|
||||||
|
local dv = peek d {
|
||||||
|
"0" => { print("found zero") 0 }
|
||||||
|
"1" => { print("found one") 1 }
|
||||||
|
else => { print("other") 0 }
|
||||||
|
}
|
||||||
|
return dv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
must_use Notes
|
||||||
|
- Peek arms are expressions. When using a block arm `{ ... }`, the last expression is the resulting value; statements without a final expression yield no usable value.
|
||||||
|
- Ternary is an expression; ensure both branches are type-compatible at MIR level (e.g., both yield integer or both yield string handle in current phase).
|
||||||
|
|
||||||
When you need the implementation details
|
When you need the implementation details
|
||||||
- Tokenizer: src/tokenizer.rs
|
- Tokenizer: src/tokenizer.rs
|
||||||
- Parser: src/parser/expressions.rs, src/parser/statements.rs
|
- Parser: src/parser/expressions.rs, src/parser/statements.rs
|
||||||
|
|||||||
59
docs/reference/architecture/parser_mvp_stage2.md
Normal file
59
docs/reference/architecture/parser_mvp_stage2.md
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
# Parser MVP Stage-2 Design (Phase 15)
|
||||||
|
|
||||||
|
Scope
|
||||||
|
- Expand Stage-1 (const/binop/return) to cover the minimal everyday surface:
|
||||||
|
- local bindings: `local x = expr`
|
||||||
|
- if/else: `if cond { ... } else { ... }`
|
||||||
|
- loop: `loop(cond) { ... }` and `loop cond { ... }`
|
||||||
|
- call: `f(a, b)` (free function)
|
||||||
|
- method: `obj.method(a, b)`
|
||||||
|
- constructor: `new BoxType(args)`
|
||||||
|
- implicit receiver `me` in box methods
|
||||||
|
- String methods: `substring`, `length`, `lastIndexOf`
|
||||||
|
|
||||||
|
Grammar Sketch (incremental)
|
||||||
|
- Expression
|
||||||
|
- precedence keeps Phase 12.7: ternary inside pipe; ternary lowers to if/branch
|
||||||
|
- postfix: method call, field access
|
||||||
|
- primary: literals, identifiers, parenthesized
|
||||||
|
- Statement
|
||||||
|
- `local ident = expr` (no rebind in Stage-2)
|
||||||
|
- expression statement (for side effects)
|
||||||
|
- return
|
||||||
|
|
||||||
|
AST Nodes (delta)
|
||||||
|
- Add nodes for Local, If, Loop, Call, MethodCall, New, Identifier, Return.
|
||||||
|
- Keep Peek and Ternary in the AST; lower to MIR via the same builder hooks.
|
||||||
|
|
||||||
|
Lowering to MIR JSON v0
|
||||||
|
- Local → assign to fresh value id, store into vmap
|
||||||
|
- If/Ternary → compare + branch blocks; PHI for join values when used as expression
|
||||||
|
- Loop → cond block → body block → back-edge; loop-carried values explicitly phied
|
||||||
|
- Call → `call { func: "name", args: [...] }`
|
||||||
|
- Method → `boxcall { box: vid, method: "name", args: [...] }`
|
||||||
|
- New → `newbox { type: "BoxType", args: [...] }`
|
||||||
|
- String ops
|
||||||
|
- `length()` → i64 length on string handle (VM: Python len; LLVM: handle→ptr at call sites only)
|
||||||
|
- `substring(a,b)` → `nyash.string.substring_hh` equivalent
|
||||||
|
- `lastIndexOf(x)` → returns i64 index or -1
|
||||||
|
|
||||||
|
Type Meta (emitted with JSON where needed)
|
||||||
|
- Compare: when both sides are string → `cmp_kind: "string"`
|
||||||
|
- BinOp `+`: if `dst_type = { kind: "handle", box_type: "StringBox" }`, force concat_hh
|
||||||
|
- Known APIs: annotate `dst_type` for Console.println (i64), dirname/join/read as `StringBox(handle)`
|
||||||
|
|
||||||
|
Policy & Invariants
|
||||||
|
- Resolver-only: do not wire PHIs in-lowering; snapshots + `finalize_phis` bind incomings
|
||||||
|
- Strings: inter-block are handle(i64) only; i8* materialized at call sites
|
||||||
|
- PHI placeholders: created at block heads from JSON-declared phi
|
||||||
|
|
||||||
|
Acceptance
|
||||||
|
- Parity on:
|
||||||
|
- `apps/tests/min_str_cat_loop/main.nyash`
|
||||||
|
- `apps/tests/esc_dirname_smoke.nyash`
|
||||||
|
- Stage-2 new smokes (ternary nested, peek return)
|
||||||
|
- `tools/parity.sh --lhs pyvm --rhs llvmlite --show-diff <app>` → green
|
||||||
|
|
||||||
|
Notes
|
||||||
|
- Keep Stage-2 additive and test-driven; avoid broad refactors.
|
||||||
|
- Future: numeric methods, map/array minimal, and using/namespace gate.
|
||||||
@ -82,6 +82,7 @@ def lower_externcall(
|
|||||||
# Prepare/coerce arguments
|
# Prepare/coerce arguments
|
||||||
call_args: List[ir.Value] = []
|
call_args: List[ir.Value] = []
|
||||||
for i, arg_id in enumerate(args):
|
for i, arg_id in enumerate(args):
|
||||||
|
orig_arg_id = arg_id
|
||||||
# Prefer resolver
|
# Prefer resolver
|
||||||
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
|
||||||
if len(func.args) > i and isinstance(func.args[i].type, ir.PointerType):
|
if len(func.args) > i and isinstance(func.args[i].type, ir.PointerType):
|
||||||
@ -99,19 +100,47 @@ def lower_externcall(
|
|||||||
expected_ty = func.args[i].type
|
expected_ty = func.args[i].type
|
||||||
if isinstance(expected_ty, ir.PointerType):
|
if isinstance(expected_ty, ir.PointerType):
|
||||||
# Need pointer
|
# Need pointer
|
||||||
if hasattr(aval, 'type'):
|
# Prefer string literal pointer or handle->i8* bridge when argument is string-ish
|
||||||
if isinstance(aval.type, ir.IntType):
|
used_string_h2p = False
|
||||||
aval = builder.inttoptr(aval, expected_ty, name=f"ext_i2p_arg{i}")
|
try:
|
||||||
elif not aval.type.is_pointer:
|
if resolver is not None and hasattr(resolver, 'string_ptrs'):
|
||||||
aval = ir.Constant(expected_ty, None)
|
sp = resolver.string_ptrs.get(orig_arg_id)
|
||||||
else:
|
if sp is not None:
|
||||||
# Pointer but wrong element type: if pointer-to-array -> GEP to i8*
|
aval = sp
|
||||||
try:
|
used_string_h2p = True
|
||||||
if isinstance(aval.type.pointee, ir.ArrayType) and isinstance(expected_ty.pointee, ir.IntType) and expected_ty.pointee.width == 8:
|
if not used_string_h2p and resolver is not None and hasattr(resolver, 'is_stringish') and resolver.is_stringish(orig_arg_id):
|
||||||
c0 = ir.Constant(ir.IntType(32), 0)
|
# Declare nyash.string.to_i8p_h(i64) and call with handle
|
||||||
aval = builder.gep(aval, [c0, c0], name=f"ext_gep_arg{i}")
|
i64 = ir.IntType(64)
|
||||||
except Exception:
|
i8p = ir.IntType(8).as_pointer()
|
||||||
pass
|
to_i8p = None
|
||||||
|
for f in module.functions:
|
||||||
|
if f.name == 'nyash.string.to_i8p_h':
|
||||||
|
to_i8p = f; break
|
||||||
|
if to_i8p is None:
|
||||||
|
to_i8p = ir.Function(module, ir.FunctionType(i8p, [i64]), name='nyash.string.to_i8p_h')
|
||||||
|
# Ensure we have an i64 handle to pass
|
||||||
|
if hasattr(aval, 'type') and isinstance(aval.type, ir.PointerType):
|
||||||
|
aval = builder.ptrtoint(aval, i64, name=f"ext_p2h_{i}")
|
||||||
|
elif hasattr(aval, 'type') and isinstance(aval.type, ir.IntType) and aval.type.width != 64:
|
||||||
|
aval = builder.zext(aval, i64, name=f"ext_zext_h_{i}")
|
||||||
|
aval = builder.call(to_i8p, [aval], name=f"ext_h2p_arg{i}")
|
||||||
|
used_string_h2p = True
|
||||||
|
except Exception:
|
||||||
|
used_string_h2p = used_string_h2p or False
|
||||||
|
if not used_string_h2p:
|
||||||
|
if hasattr(aval, 'type'):
|
||||||
|
if isinstance(aval.type, ir.IntType):
|
||||||
|
aval = builder.inttoptr(aval, expected_ty, name=f"ext_i2p_arg{i}")
|
||||||
|
elif not aval.type.is_pointer:
|
||||||
|
aval = ir.Constant(expected_ty, None)
|
||||||
|
else:
|
||||||
|
# Pointer but wrong element type: if pointer-to-array -> GEP to i8*
|
||||||
|
try:
|
||||||
|
if isinstance(aval.type.pointee, ir.ArrayType) and isinstance(expected_ty.pointee, ir.IntType) and expected_ty.pointee.width == 8:
|
||||||
|
c0 = ir.Constant(ir.IntType(32), 0)
|
||||||
|
aval = builder.gep(aval, [c0, c0], name=f"ext_gep_arg{i}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
aval = ir.Constant(expected_ty, None)
|
aval = ir.Constant(expected_ty, None)
|
||||||
elif isinstance(expected_ty, ir.IntType) and expected_ty.width == 64:
|
elif isinstance(expected_ty, ir.IntType) and expected_ty.width == 64:
|
||||||
|
|||||||
@ -9,42 +9,81 @@ impl super::MirBuilder {
|
|||||||
arms: Vec<(LiteralValue, ASTNode)>,
|
arms: Vec<(LiteralValue, ASTNode)>,
|
||||||
else_expr: ASTNode,
|
else_expr: ASTNode,
|
||||||
) -> Result<ValueId, String> {
|
) -> Result<ValueId, String> {
|
||||||
|
// Evaluate scrutinee in the current block
|
||||||
let scr_val = self.build_expression_impl(scrutinee)?;
|
let scr_val = self.build_expression_impl(scrutinee)?;
|
||||||
|
|
||||||
|
// Prepare merge and result
|
||||||
let merge_block: BasicBlockId = self.block_gen.next();
|
let merge_block: BasicBlockId = self.block_gen.next();
|
||||||
let result_val = self.value_gen.next();
|
let result_val = self.value_gen.next();
|
||||||
let mut phi_inputs: Vec<(BasicBlockId, ValueId)> = Vec::new();
|
let mut phi_inputs: Vec<(BasicBlockId, ValueId)> = Vec::new();
|
||||||
let mut next_block = self.block_gen.next();
|
|
||||||
self.start_new_block(next_block)?;
|
|
||||||
|
|
||||||
|
// Create dispatch block where we start comparing arms
|
||||||
|
let dispatch_block = self.block_gen.next();
|
||||||
|
// Jump from current block to dispatch (ensure terminator exists)
|
||||||
|
let need_jump = {
|
||||||
|
let cur = self.current_block;
|
||||||
|
if let (Some(cb), Some(ref func)) = (cur, &self.current_function) {
|
||||||
|
if let Some(bb) = func.blocks.get(&cb) { !bb.is_terminated() } else { true }
|
||||||
|
} else { true }
|
||||||
|
};
|
||||||
|
if need_jump {
|
||||||
|
self.emit_instruction(super::MirInstruction::Jump { target: dispatch_block })?;
|
||||||
|
}
|
||||||
|
self.start_new_block(dispatch_block)?;
|
||||||
|
|
||||||
|
// If there are no arms, fall through to else directly
|
||||||
|
if arms.is_empty() {
|
||||||
|
let else_block = self.block_gen.next();
|
||||||
|
self.emit_instruction(super::MirInstruction::Jump { target: else_block })?;
|
||||||
|
self.start_new_block(else_block)?;
|
||||||
|
let else_val = self.build_expression_impl(else_expr)?;
|
||||||
|
phi_inputs.push((else_block, else_val));
|
||||||
|
self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?;
|
||||||
|
self.start_new_block(merge_block)?;
|
||||||
|
self.emit_instruction(super::MirInstruction::Phi { dst: result_val, inputs: phi_inputs })?;
|
||||||
|
return Ok(result_val);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Else block to handle default case
|
||||||
|
let else_block = self.block_gen.next();
|
||||||
|
|
||||||
|
// Chain dispatch blocks for each arm
|
||||||
|
let mut cur_dispatch = dispatch_block;
|
||||||
for (i, (label, arm_expr)) in arms.iter().cloned().enumerate() {
|
for (i, (label, arm_expr)) in arms.iter().cloned().enumerate() {
|
||||||
let then_block = self.block_gen.next();
|
let then_block = self.block_gen.next();
|
||||||
// Only string labels handled here (behavior unchanged)
|
// Next dispatch (only for non-last arm)
|
||||||
|
let next_dispatch = if i + 1 < arms.len() { Some(self.block_gen.next()) } else { None };
|
||||||
|
let else_target = next_dispatch.unwrap_or(else_block);
|
||||||
|
|
||||||
|
// In current dispatch block, compare and branch
|
||||||
|
self.start_new_block(cur_dispatch)?;
|
||||||
if let LiteralValue::String(s) = label {
|
if let LiteralValue::String(s) = label {
|
||||||
let lit_id = self.value_gen.next();
|
let lit_id = self.value_gen.next();
|
||||||
self.emit_instruction(super::MirInstruction::Const { dst: lit_id, value: super::ConstValue::String(s) })?;
|
self.emit_instruction(super::MirInstruction::Const { dst: lit_id, value: super::ConstValue::String(s) })?;
|
||||||
let cond_id = self.value_gen.next();
|
let cond_id = self.value_gen.next();
|
||||||
self.emit_instruction(super::MirInstruction::Compare { dst: cond_id, op: super::CompareOp::Eq, lhs: scr_val, rhs: lit_id })?;
|
self.emit_instruction(super::MirInstruction::Compare { dst: cond_id, op: super::CompareOp::Eq, lhs: scr_val, rhs: lit_id })?;
|
||||||
self.emit_instruction(super::MirInstruction::Branch { condition: cond_id, then_bb: then_block, else_bb: next_block })?;
|
self.emit_instruction(super::MirInstruction::Branch { condition: cond_id, then_bb: then_block, else_bb: else_target })?;
|
||||||
self.start_new_block(then_block)?;
|
|
||||||
let then_val = self.build_expression_impl(arm_expr)?;
|
|
||||||
phi_inputs.push((then_block, then_val));
|
|
||||||
self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?;
|
|
||||||
if i < arms.len() - 1 {
|
|
||||||
let b = self.block_gen.next();
|
|
||||||
self.start_new_block(b)?;
|
|
||||||
next_block = b;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// then arm
|
||||||
|
self.start_new_block(then_block)?;
|
||||||
|
let then_val = self.build_expression_impl(arm_expr)?;
|
||||||
|
phi_inputs.push((then_block, then_val));
|
||||||
|
self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?;
|
||||||
|
|
||||||
|
// Move to next dispatch or else block
|
||||||
|
cur_dispatch = else_target;
|
||||||
}
|
}
|
||||||
|
|
||||||
let else_block_id = next_block;
|
// Lower else expression in else_block
|
||||||
self.start_new_block(else_block_id)?;
|
self.start_new_block(else_block)?;
|
||||||
let else_val = self.build_expression_impl(else_expr)?;
|
let else_val = self.build_expression_impl(else_expr)?;
|
||||||
phi_inputs.push((else_block_id, else_val));
|
phi_inputs.push((else_block, else_val));
|
||||||
self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?;
|
self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?;
|
||||||
|
|
||||||
|
// Merge and yield result
|
||||||
self.start_new_block(merge_block)?;
|
self.start_new_block(merge_block)?;
|
||||||
self.emit_instruction(super::MirInstruction::Phi { dst: result_val, inputs: phi_inputs })?;
|
self.emit_instruction(super::MirInstruction::Phi { dst: result_val, inputs: phi_inputs })?;
|
||||||
Ok(result_val)
|
Ok(result_val)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -98,7 +98,7 @@ impl NyashParser {
|
|||||||
condition: Box::new(cond),
|
condition: Box::new(cond),
|
||||||
then_body: vec![then_expr],
|
then_body: vec![then_expr],
|
||||||
else_body: Some(vec![else_expr]),
|
else_body: Some(vec![else_expr]),
|
||||||
span: Span::Unknown,
|
span: Span::unknown(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(cond)
|
Ok(cond)
|
||||||
|
|||||||
@ -151,15 +151,14 @@ impl NyashRunner {
|
|||||||
.status()
|
.status()
|
||||||
.map_err(|e| format!("spawn pyvm: {}", e))
|
.map_err(|e| format!("spawn pyvm: {}", e))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
// Always propagate PyVM exit code to match llvmlite semantics
|
||||||
|
let code = status.code().unwrap_or(1);
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
eprintln!("❌ PyVM failed (status={})", status.code().unwrap_or(-1));
|
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
|
||||||
process::exit(1);
|
eprintln!("❌ PyVM failed (status={})", code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Propagate exit code if set
|
process::exit(code);
|
||||||
if let Some(code) = status.code() {
|
|
||||||
process::exit(code);
|
|
||||||
}
|
|
||||||
process::exit(0);
|
|
||||||
} else {
|
} else {
|
||||||
eprintln!("❌ PyVM runner not found: {}", runner.display());
|
eprintln!("❌ PyVM runner not found: {}", runner.display());
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
|
|||||||
@ -67,6 +67,7 @@ normalize() {
|
|||||||
-e '/^🔌/d' \
|
-e '/^🔌/d' \
|
||||||
-e '/^✅/d' \
|
-e '/^✅/d' \
|
||||||
-e '/^🚀/d' \
|
-e '/^🚀/d' \
|
||||||
|
-e '/^❌/d' \
|
||||||
-e '/^⚡/d' \
|
-e '/^⚡/d' \
|
||||||
-e '/^🦀/d' \
|
-e '/^🦀/d' \
|
||||||
-e '/^🧠/d' \
|
-e '/^🧠/d' \
|
||||||
|
|||||||
Reference in New Issue
Block a user