@tishlang/tish 1.0.33 → 1.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "1.0.33"
3
+ version = "1.0.34"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -443,6 +443,9 @@ pub fn compile_with_native_modules(
443
443
  optimize: bool,
444
444
  ) -> Result<String, CompileError> {
445
445
  let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
446
+ // Type-inference pass: fills in `type_ann` on unannotated VarDecl nodes where
447
+ // the type is unambiguous (literals, arithmetic of typed vars, etc.).
448
+ let program = crate::infer::infer_program(&program);
446
449
  let map: std::collections::HashMap<String, (String, String)> = native_modules
447
450
  .iter()
448
451
  .map(|m| (m.spec.clone(), (m.crate_name.clone(), m.export_fn.clone())))
@@ -706,7 +709,18 @@ impl Codegen {
706
709
  fn emit_inc_dec(&self, name: &str, is_prefix: bool, delta: &str, op_name: &str) -> String {
707
710
  let n = Self::escape_ident(name);
708
711
  let is_wrapped = self.refcell_wrapped_vars.contains(name);
709
-
712
+ let var_type = self.type_context.get_type(name);
713
+
714
+ // Native fast path: f64 variable → avoid boxing/unboxing.
715
+ if !is_wrapped && var_type == RustType::F64 {
716
+ let op_assign = if delta.contains('+') { "+=" } else { "-=" };
717
+ return if is_prefix {
718
+ format!("{{ {n} {op_assign} 1.0_f64; Value::Number({n}) }}")
719
+ } else {
720
+ format!("{{ let _prev = {n}; {n} {op_assign} 1.0_f64; Value::Number(_prev) }}")
721
+ };
722
+ }
723
+
710
724
  if is_prefix {
711
725
  if is_wrapped {
712
726
  format!(
@@ -717,16 +731,14 @@ impl Codegen {
717
731
  "{{ {n} = Value::Number(match &{n} {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); {n}.clone() }}"
718
732
  )
719
733
  }
734
+ } else if is_wrapped {
735
+ format!(
736
+ "{{ let _v = (*{n}.borrow()).clone(); *{n}.borrow_mut() = Value::Number(match &_v {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); _v }}"
737
+ )
720
738
  } else {
721
- if is_wrapped {
722
- format!(
723
- "{{ let _v = (*{n}.borrow()).clone(); *{n}.borrow_mut() = Value::Number(match &_v {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); _v }}"
724
- )
725
- } else {
726
- format!(
727
- "{{ let _v = {n}.clone(); {n} = Value::Number(match &_v {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); _v }}"
728
- )
729
- }
739
+ format!(
740
+ "{{ let _v = {n}.clone(); {n} = Value::Number(match &_v {{ Value::Number(n) => n {delta}, _ => panic!(\"{op_name} needs number\") }}); _v }}"
741
+ )
730
742
  }
731
743
  }
732
744
 
@@ -1133,8 +1145,8 @@ impl Codegen {
1133
1145
  else_branch,
1134
1146
  ..
1135
1147
  } => {
1136
- let c = self.emit_expr(cond)?;
1137
- self.write(&format!("if {}.is_truthy() {{\n", c));
1148
+ let c = self.emit_cond_expr(cond)?;
1149
+ self.write(&format!("if {} {{\n", c));
1138
1150
  self.indent += 1;
1139
1151
  self.emit_statement(then_branch)?;
1140
1152
  self.indent -= 1;
@@ -1147,11 +1159,11 @@ impl Codegen {
1147
1159
  self.writeln("}");
1148
1160
  }
1149
1161
  Statement::While { cond, body, .. } => {
1150
- let c = self.emit_expr(cond)?;
1162
+ let c = self.emit_cond_expr(cond)?;
1151
1163
  let label = format!("'while_loop_{}", self.loop_label_index);
1152
1164
  self.loop_label_index += 1;
1153
1165
  self.loop_stack.push((label.clone(), None));
1154
- self.write(&format!("{}: while {}.is_truthy() {{\n", label, c));
1166
+ self.write(&format!("{}: while {} {{\n", label, c));
1155
1167
  self.indent += 1;
1156
1168
  self.emit_statement(body)?;
1157
1169
  self.loop_stack.pop();
@@ -1209,7 +1221,7 @@ impl Codegen {
1209
1221
  self.loop_label_index += 1;
1210
1222
  let cond_expr = cond
1211
1223
  .as_ref()
1212
- .map(|c| format!("{}.is_truthy()", self.emit_expr(c).unwrap()))
1224
+ .map(|c| self.emit_cond_expr(c).unwrap())
1213
1225
  .unwrap_or_else(|| "true".to_string());
1214
1226
  let update_code = update.as_ref().map(|u| {
1215
1227
  let ue = self.emit_expr(u).unwrap();
@@ -1301,14 +1313,14 @@ impl Codegen {
1301
1313
  self.writeln("}");
1302
1314
  }
1303
1315
  Statement::DoWhile { body, cond, .. } => {
1304
- let c = self.emit_expr(cond)?;
1316
+ let c = self.emit_cond_expr(cond)?;
1305
1317
  let label = format!("'dowhile_loop_{}", self.loop_label_index);
1306
1318
  self.loop_label_index += 1;
1307
1319
  self.loop_stack.push((label.clone(), None));
1308
1320
  self.write(&format!("{}: loop {{\n", label));
1309
1321
  self.indent += 1;
1310
1322
  self.emit_statement(body)?;
1311
- self.write(&format!("if !{}.is_truthy() {{ break; }}\n", c));
1323
+ self.write(&format!("if !{} {{ break; }}\n", c));
1312
1324
  self.loop_stack.pop();
1313
1325
  self.indent -= 1;
1314
1326
  self.writeln("}");
@@ -1715,10 +1727,10 @@ impl Codegen {
1715
1727
  }
1716
1728
  }
1717
1729
  }
1718
- Expr::Binary { left, op, right, span, .. } => {
1719
- let l = self.emit_expr(left)?;
1720
- let r = self.emit_expr(right)?;
1721
- self.emit_binop(&l, *op, &r, *span)?
1730
+ Expr::Binary { .. } => {
1731
+ // Delegate to emit_typed_expr; wrap the native result in Value.
1732
+ let (code, ty) = self.emit_typed_expr(expr)?;
1733
+ if ty.is_native() { ty.to_value_expr(&code) } else { code }
1722
1734
  }
1723
1735
  Expr::Unary { op, operand, .. } => {
1724
1736
  let o = self.emit_expr(operand)?;
@@ -1779,6 +1791,37 @@ impl Codegen {
1779
1791
 
1780
1792
  // Check for built-in method calls on arrays/strings
1781
1793
  if let Expr::Member { object, prop: MemberProp::Name(method_name), .. } = callee.as_ref() {
1794
+ // ── native Vec<T> push fast path ──────────────────────────────
1795
+ if method_name.as_ref() == "push" {
1796
+ if let Expr::Ident { name, .. } = object.as_ref() {
1797
+ if !self.refcell_wrapped_vars.contains(name.as_ref()) {
1798
+ let obj_type = self.type_context.get_type(name.as_ref());
1799
+ if let RustType::Vec(elem_type) = obj_type {
1800
+ let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
1801
+ // Collect push arguments as native values.
1802
+ let mut push_stmts: Vec<String> = Vec::new();
1803
+ for a in args {
1804
+ if let CallArg::Expr(e) = a {
1805
+ let (val_code, val_ty) = self.emit_typed_expr(e)?;
1806
+ let native_val = if val_ty == *elem_type {
1807
+ val_code
1808
+ } else if val_ty == RustType::Value {
1809
+ elem_type.from_value_expr(&val_code)
1810
+ } else {
1811
+ val_code
1812
+ };
1813
+ push_stmts.push(format!("{}.push({});", esc_obj, native_val));
1814
+ }
1815
+ }
1816
+ return Ok(format!(
1817
+ "{{ {} Value::Null }}",
1818
+ push_stmts.join(" ")
1819
+ ));
1820
+ }
1821
+ }
1822
+ }
1823
+ }
1824
+
1782
1825
  let obj_expr = self.emit_expr(object)?;
1783
1826
  let arg_exprs: Result<Vec<_>, _> =
1784
1827
  args.iter().map(|a| self.emit_call_arg(a)).collect();
@@ -2136,23 +2179,24 @@ impl Codegen {
2136
2179
  format!("tishlang_runtime::get_prop(&{}, {})", obj, key)
2137
2180
  }
2138
2181
  }
2182
+ Expr::Index { optional, .. } if !optional => {
2183
+ // Try native Vec<T> fast path via emit_typed_expr; wrap result.
2184
+ let (code, ty) = self.emit_typed_expr(expr)?;
2185
+ if ty.is_native() { ty.to_value_expr(&code) } else { code }
2186
+ }
2139
2187
  Expr::Index {
2140
2188
  object,
2141
2189
  index,
2142
- optional,
2143
2190
  ..
2144
2191
  } => {
2192
+ // optional chaining: always use runtime path
2145
2193
  let obj = self.emit_expr(object)?;
2146
2194
  let idx = self.emit_expr(index)?;
2147
- if *optional {
2148
- format!(
2149
- "{{ let o = {}.clone(); if matches!(o, Value::Null) {{ Value::Null }} else {{ \
2150
- tishlang_runtime::get_index(&o, &{}) }} }}",
2151
- obj, idx
2152
- )
2153
- } else {
2154
- format!("tishlang_runtime::get_index(&{}, &{})", obj, idx)
2155
- }
2195
+ format!(
2196
+ "{{ let o = {}.clone(); if matches!(o, Value::Null) {{ Value::Null }} else {{ \
2197
+ tishlang_runtime::get_index(&o, &{}) }} }}",
2198
+ obj, idx
2199
+ )
2156
2200
  }
2157
2201
  Expr::Conditional {
2158
2202
  cond,
@@ -2257,8 +2301,29 @@ impl Codegen {
2257
2301
  }
2258
2302
  }
2259
2303
  Expr::Assign { name, value, .. } => {
2260
- let val = self.emit_expr(value)?;
2261
2304
  let escaped = Self::escape_ident(name.as_ref());
2305
+ // Native fast path: if the target is a scalar native type, emit
2306
+ // a direct assignment without boxing/unboxing through Value.
2307
+ if !self.refcell_wrapped_vars.contains(name.as_ref()) {
2308
+ let rust_type = self.type_context.get_type(name.as_ref());
2309
+ if rust_type.is_native() && matches!(rust_type, RustType::F64 | RustType::Bool | RustType::String) {
2310
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
2311
+ let native_val = if val_ty == rust_type {
2312
+ val_code
2313
+ } else if val_ty == RustType::Value {
2314
+ rust_type.from_value_expr(&val_code)
2315
+ } else {
2316
+ val_code
2317
+ };
2318
+ let return_val = rust_type.to_value_expr(&escaped);
2319
+ return Ok(format!(
2320
+ "{{ {} = {}; {} }}",
2321
+ escaped, native_val, return_val
2322
+ ));
2323
+ }
2324
+ }
2325
+ // Fallback: Value path
2326
+ let val = self.emit_expr(value)?;
2262
2327
  let needs_outer_clone = self.should_clone(value);
2263
2328
  if self.refcell_wrapped_vars.contains(name.as_ref()) {
2264
2329
  if needs_outer_clone {
@@ -2267,7 +2332,6 @@ impl Codegen {
2267
2332
  format!("{{ let _v = {}; *{}.borrow_mut() = _v.clone(); _v }}", val, escaped)
2268
2333
  }
2269
2334
  } else {
2270
- // Use type_context: typed vars need from_value_expr; Value needs .clone() (we return _v)
2271
2335
  let rust_type = self.type_context.get_type(name.as_ref());
2272
2336
  let assign_rhs = if matches!(rust_type, RustType::Value) {
2273
2337
  "_v.clone()".to_string()
@@ -2326,8 +2390,65 @@ impl Codegen {
2326
2390
  Expr::PrefixInc { name, .. } => self.emit_inc_dec(name.as_ref(), true, "+ 1.0", "++"),
2327
2391
  Expr::PrefixDec { name, .. } => self.emit_inc_dec(name.as_ref(), true, "- 1.0", "--"),
2328
2392
  Expr::CompoundAssign { name, op, value, .. } => {
2329
- let val = self.emit_expr(value)?;
2330
2393
  let n = Self::escape_ident(name.as_ref());
2394
+ let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
2395
+ let var_type = self.type_context.get_type(name.as_ref());
2396
+
2397
+ // ── native f64 fast path: direct arithmetic operators ─────────
2398
+ // emit_expr must return a Value expression; wrap the result back.
2399
+ if !is_refcell && var_type == RustType::F64 {
2400
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
2401
+ let rhs_f64 = if rhs_ty == RustType::F64 {
2402
+ rhs_code
2403
+ } else {
2404
+ // rhs is Value or another native: unbox to f64
2405
+ let rhs_val = if rhs_ty.is_native() {
2406
+ rhs_ty.to_value_expr(&rhs_code)
2407
+ } else {
2408
+ rhs_code
2409
+ };
2410
+ format!("(match &({}) {{ Value::Number(n) => *n, v => panic!(\"compound assign: expected number, got {{:?}}\", v) }})", rhs_val)
2411
+ };
2412
+ let op_str = match op {
2413
+ CompoundOp::Add => "+=",
2414
+ CompoundOp::Sub => "-=",
2415
+ CompoundOp::Mul => "*=",
2416
+ CompoundOp::Div => "/=",
2417
+ CompoundOp::Mod => "%=",
2418
+ };
2419
+ // Wrap in Value::Number so the expression is a valid Value
2420
+ return Ok(format!("{{ {} {} {}; Value::Number({}) }}", n, op_str, rhs_f64, n));
2421
+ }
2422
+
2423
+ // ── native String += fast path: push_str ─────────────────────
2424
+ if !is_refcell && var_type == RustType::String && matches!(op, CompoundOp::Add) {
2425
+ let (rhs_code, rhs_ty) = self.emit_typed_expr(value)?;
2426
+ let rhs_str = if rhs_ty == RustType::String {
2427
+ rhs_code
2428
+ } else {
2429
+ // Convert rhs Value to display string inline
2430
+ let rhs_val = if rhs_ty.is_native() {
2431
+ rhs_ty.to_value_expr(&rhs_code)
2432
+ } else {
2433
+ rhs_code
2434
+ };
2435
+ format!(
2436
+ "match &({}) {{ \
2437
+ Value::String(s) => s.to_string(), \
2438
+ Value::Number(n) => {{ let i = *n as i64; if (*n - i as f64).abs() < f64::EPSILON {{ i.to_string() }} else {{ n.to_string() }} }}, \
2439
+ Value::Bool(b) => b.to_string(), \
2440
+ Value::Null => \"null\".to_string(), \
2441
+ other => format!(\"{{:?}}\", other) }}",
2442
+ rhs_val
2443
+ )
2444
+ };
2445
+ // Wrap in Value::String so the expression is a valid Value
2446
+ return Ok(format!("{{ {}.push_str(&({})); Value::String({}.clone().into()) }}", n, rhs_str, n));
2447
+ }
2448
+
2449
+ // ── fallback: Value path ──────────────────────────────────────
2450
+ // If the variable is native, wrap it as Value before calling ops::
2451
+ let val = self.emit_expr(value)?;
2331
2452
  let op_fn = match op {
2332
2453
  CompoundOp::Add => "add",
2333
2454
  CompoundOp::Sub => "sub",
@@ -2335,11 +2456,20 @@ impl Codegen {
2335
2456
  CompoundOp::Div => "div",
2336
2457
  CompoundOp::Mod => "modulo",
2337
2458
  };
2338
- if self.refcell_wrapped_vars.contains(name.as_ref()) {
2459
+ if is_refcell {
2339
2460
  format!(
2340
2461
  "{{ let _rhs = ({}).clone(); *{}.borrow_mut() = tishlang_runtime::ops::{}(&*{}.borrow(), &_rhs)?; (*{}.borrow()).clone() }}",
2341
2462
  val, n, op_fn, n, n
2342
2463
  )
2464
+ } else if var_type.is_native() {
2465
+ // Wrap native lhs as Value, run ops::, unbox result back to native
2466
+ let n_as_value = var_type.to_value_expr(&n);
2467
+ let result_native = var_type.from_value_expr("_result");
2468
+ let n_as_value2 = var_type.to_value_expr(&n);
2469
+ format!(
2470
+ "{{ let _lhs = {}; let _rhs = ({}).clone(); let _result = tishlang_runtime::ops::{}(&_lhs, &_rhs)?; {} = {}; {} }}",
2471
+ n_as_value, val, op_fn, n, result_native, n_as_value2
2472
+ )
2343
2473
  } else {
2344
2474
  format!(
2345
2475
  "{{ let _rhs = ({}).clone(); {} = tishlang_runtime::ops::{}(&{}, &_rhs)?; {}.clone() }}",
@@ -2351,6 +2481,35 @@ impl Codegen {
2351
2481
  let val = self.emit_expr(value)?;
2352
2482
  let n = Self::escape_ident(name.as_ref()).into_owned();
2353
2483
  let is_refcell = self.refcell_wrapped_vars.contains(name.as_ref());
2484
+ let var_type = self.type_context.get_type(name.as_ref());
2485
+
2486
+ // ── native type: wrap for condition, unbox for assignment ──────
2487
+ if !is_refcell && var_type.is_native() {
2488
+ // n_as_value uses .clone() for String so we don't consume n
2489
+ let n_as_value = var_type.to_value_expr(&n);
2490
+ let val_as_native = var_type.from_value_expr("_v");
2491
+ let (cond, assign_and_return, else_expr) = match op {
2492
+ LogicalAssignOp::AndAnd => (
2493
+ format!("{{ let __chk = {}; __chk.is_truthy() }}", n_as_value),
2494
+ format!("{{ let _v = ({}).clone(); {} = {}; {} }}", val, n, val_as_native, var_type.to_value_expr(&n)),
2495
+ var_type.to_value_expr(&n),
2496
+ ),
2497
+ LogicalAssignOp::OrOr => (
2498
+ format!("!{{ let __chk = {}; __chk.is_truthy() }}", n_as_value),
2499
+ format!("{{ let _v = ({}).clone(); {} = {}; {} }}", val, n, val_as_native, var_type.to_value_expr(&n)),
2500
+ var_type.to_value_expr(&n),
2501
+ ),
2502
+ // Native types (f64, String, bool) are never null — ??= is a no-op
2503
+ LogicalAssignOp::Nullish => (
2504
+ "false".to_string(),
2505
+ var_type.to_value_expr(&n), // unreachable but must type-check
2506
+ var_type.to_value_expr(&n),
2507
+ ),
2508
+ };
2509
+ return Ok(format!("{{ if {} {{ {} }} else {{ {} }} }}", cond, assign_and_return, else_expr));
2510
+ }
2511
+
2512
+ // ── Value / refcell path ──────────────────────────────────────
2354
2513
  let (cond, assign_and_return, else_expr) = if is_refcell {
2355
2514
  match op {
2356
2515
  LogicalAssignOp::AndAnd => (
@@ -2401,6 +2560,43 @@ impl Codegen {
2401
2560
  )
2402
2561
  }
2403
2562
  Expr::IndexAssign { object, index, value, .. } => {
2563
+ // Native fast path: Vec<T>[i] = v
2564
+ if let Expr::Ident { name, .. } = object.as_ref() {
2565
+ if !self.refcell_wrapped_vars.contains(name.as_ref()) {
2566
+ let obj_type = self.type_context.get_type(name.as_ref());
2567
+ if let RustType::Vec(elem_type) = obj_type {
2568
+ let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
2569
+ let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
2570
+ let idx_usize = if idx_ty == RustType::F64 {
2571
+ format!("({}) as usize", idx_code)
2572
+ } else {
2573
+ let iv = if idx_ty.is_native() {
2574
+ idx_ty.to_value_expr(&idx_code)
2575
+ } else {
2576
+ idx_code
2577
+ };
2578
+ format!(
2579
+ "{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
2580
+ iv
2581
+ )
2582
+ };
2583
+ let (val_code, val_ty) = self.emit_typed_expr(value)?;
2584
+ let native_val = if val_ty == *elem_type {
2585
+ val_code
2586
+ } else if val_ty == RustType::Value {
2587
+ elem_type.from_value_expr(&val_code)
2588
+ } else {
2589
+ // both native but different type — best effort
2590
+ val_code
2591
+ };
2592
+ return Ok(format!(
2593
+ "{{ {}[{}] = {}; Value::Null }}",
2594
+ esc_obj, idx_usize, native_val
2595
+ ));
2596
+ }
2597
+ }
2598
+ }
2599
+ // Fallback: runtime set_index
2404
2600
  let obj = self.emit_expr(object)?;
2405
2601
  let idx = self.emit_expr(index)?;
2406
2602
  let val = self.emit_expr(value)?;
@@ -3438,6 +3634,146 @@ impl Codegen {
3438
3634
  Ok(target_type.from_value_expr(&value_expr))
3439
3635
  }
3440
3636
 
3637
+ /// Emit an expression and return `(code, type)`.
3638
+ ///
3639
+ /// When `type` is a native type (`F64`, `Bool`, `String`, `Vec<T>`, …), `code`
3640
+ /// evaluates to a Rust value of that type directly — **not** a `Value`.
3641
+ /// When `type` is `RustType::Value`, `code` evaluates to a `Value`.
3642
+ ///
3643
+ /// This is the fast-path used by callers that want to propagate native types
3644
+ /// through arithmetic, indexing, and assignments. For any expression this
3645
+ /// function cannot handle natively, it falls back to `emit_expr` and returns
3646
+ /// `RustType::Value`.
3647
+ fn emit_typed_expr(&mut self, expr: &Expr) -> Result<(String, RustType), CompileError> {
3648
+ match expr {
3649
+ // ── literals ─────────────────────────────────────────────────────────
3650
+ Expr::Literal { value, .. } => match value {
3651
+ Literal::Number(n) => Ok((format!("{}_f64", n), RustType::F64)),
3652
+ Literal::String(s) => Ok((format!("{:?}.to_string()", s.as_ref()), RustType::String)),
3653
+ Literal::Bool(b) => Ok((format!("{}", b), RustType::Bool)),
3654
+ Literal::Null => Ok(("Value::Null".to_string(), RustType::Value)),
3655
+ },
3656
+
3657
+ // ── identifiers ──────────────────────────────────────────────────────
3658
+ Expr::Ident { name, .. } => {
3659
+ let escaped = Self::escape_ident(name.as_ref());
3660
+ if self.refcell_wrapped_vars.contains(name.as_ref()) {
3661
+ // RefCell-wrapped: unwrap via borrow and return Value
3662
+ Ok((format!("(*{}.borrow()).clone()", escaped), RustType::Value))
3663
+ } else {
3664
+ let var_type = self.type_context.get_type(name.as_ref());
3665
+ if var_type.is_native() {
3666
+ Ok((escaped.into_owned(), var_type))
3667
+ } else {
3668
+ Ok((escaped.into_owned(), RustType::Value))
3669
+ }
3670
+ }
3671
+ }
3672
+
3673
+ // ── binary expressions ───────────────────────────────────────────────
3674
+ Expr::Binary { left, op, right, span, .. } => {
3675
+ let (l, lt) = self.emit_typed_expr(left)?;
3676
+ let (r, rt) = self.emit_typed_expr(right)?;
3677
+
3678
+ if let Some(result_ty) = RustType::result_type_of_binop(*op, &lt, &rt) {
3679
+ // Both sides are compatible native types → emit native op.
3680
+ let code = match op {
3681
+ BinOp::Add => format!("({} + {})", l, r),
3682
+ BinOp::Sub => format!("({} - {})", l, r),
3683
+ BinOp::Mul => format!("({} * {})", l, r),
3684
+ BinOp::Div => format!("({} / {})", l, r),
3685
+ BinOp::Mod => format!("({} % {})", l, r),
3686
+ BinOp::Pow => format!("({}).powf({})", l, r),
3687
+ BinOp::Lt => format!("({} < {})", l, r),
3688
+ BinOp::Le => format!("({} <= {})", l, r),
3689
+ BinOp::Gt => format!("({} > {})", l, r),
3690
+ BinOp::Ge => format!("({} >= {})", l, r),
3691
+ BinOp::StrictEq => format!("({} == {})", l, r),
3692
+ BinOp::StrictNe => format!("({} != {})", l, r),
3693
+ BinOp::And => format!("({} && {})", l, r),
3694
+ BinOp::Or => format!("({} || {})", l, r),
3695
+ _ => unreachable!("result_type_of_binop covers all handled ops"),
3696
+ };
3697
+ return Ok((code, result_ty));
3698
+ }
3699
+
3700
+ // Fall back: convert both sides to Value and use the runtime.
3701
+ let lv = if lt.is_native() { lt.to_value_expr(&l) } else { l };
3702
+ let rv = if rt.is_native() { rt.to_value_expr(&r) } else { r };
3703
+ let result = self.emit_binop(&lv, *op, &rv, *span)?;
3704
+ Ok((result, RustType::Value))
3705
+ }
3706
+
3707
+ // ── array indexing ───────────────────────────────────────────────────
3708
+ Expr::Index { object, index, optional, .. } => {
3709
+ // Native fast path: `vec[i]` where vec is Vec<T> and i is numeric.
3710
+ if !optional {
3711
+ if let Expr::Ident { name, .. } = object.as_ref() {
3712
+ if !self.refcell_wrapped_vars.contains(name.as_ref()) {
3713
+ let obj_type = self.type_context.get_type(name.as_ref());
3714
+ if let RustType::Vec(elem_type) = &obj_type {
3715
+ let esc_obj = Self::escape_ident(name.as_ref()).into_owned();
3716
+ let (idx_code, idx_ty) = self.emit_typed_expr(index)?;
3717
+ let idx_usize = if idx_ty == RustType::F64 {
3718
+ format!("({}) as usize", idx_code)
3719
+ } else {
3720
+ let iv = if idx_ty.is_native() {
3721
+ idx_ty.to_value_expr(&idx_code)
3722
+ } else {
3723
+ idx_code
3724
+ };
3725
+ format!(
3726
+ "{{ let _i = &{}; if let Value::Number(n) = _i {{ *n as usize }} else {{ panic!(\"array index must be a number\") }} }}",
3727
+ iv
3728
+ )
3729
+ };
3730
+ let elem_ty = *elem_type.clone();
3731
+ return Ok((format!("{}[{}]", esc_obj, idx_usize), elem_ty));
3732
+ }
3733
+ }
3734
+ }
3735
+ }
3736
+ // Value fallback: emit runtime code directly to avoid cycles
3737
+ // (emit_expr for !optional Index delegates here, so we must not call emit_expr(expr))
3738
+ let obj = self.emit_expr(object)?;
3739
+ let idx = self.emit_expr(index)?;
3740
+ let result = if *optional {
3741
+ format!(
3742
+ "{{ let o = {}.clone(); if matches!(o, Value::Null) {{ Value::Null }} else {{ \
3743
+ tishlang_runtime::get_index(&o, &{}) }} }}",
3744
+ obj, idx
3745
+ )
3746
+ } else {
3747
+ format!("tishlang_runtime::get_index(&{}, &{})", obj, idx)
3748
+ };
3749
+ Ok((result, RustType::Value))
3750
+ }
3751
+
3752
+ // ── everything else: delegate to emit_expr ───────────────────────────
3753
+ _ => {
3754
+ let result = self.emit_expr(expr)?;
3755
+ Ok((result, RustType::Value))
3756
+ }
3757
+ }
3758
+ }
3759
+
3760
+ /// Emit a condition expression as a Rust `bool`.
3761
+ ///
3762
+ /// Returns a `bool`-typed Rust expression when the condition can be
3763
+ /// determined to be native (e.g. `i < N` where both are `f64`), otherwise
3764
+ /// falls back to `{value}.is_truthy()`.
3765
+ fn emit_cond_expr(&mut self, expr: &Expr) -> Result<String, CompileError> {
3766
+ let (code, ty) = self.emit_typed_expr(expr)?;
3767
+ if ty == RustType::Bool {
3768
+ Ok(code)
3769
+ } else if ty.is_native() {
3770
+ // Non-bool native type: convert to Value and use is_truthy
3771
+ Ok(format!("{}.is_truthy()", ty.to_value_expr(&code)))
3772
+ } else {
3773
+ Ok(format!("{}.is_truthy()", code))
3774
+ }
3775
+ }
3776
+
3441
3777
  fn emit_binop(
3442
3778
  &self,
3443
3779
  l: &str,
@@ -0,0 +1,236 @@
1
+ //! Type inference pass: annotates `VarDecl` nodes with inferred `TypeAnnotation`s
2
+ //! where the user hasn't provided them, enabling codegen to emit native Rust types.
3
+ //!
4
+ //! Rules (conservative — only infer when unambiguous):
5
+ //! - Number literal init → `number`
6
+ //! - String literal init → `string`
7
+ //! - Bool literal init → `boolean`
8
+ //! - Arithmetic of two `number` expressions → `number`
9
+ //! - Comparison of two `number` expressions → `boolean`
10
+ //! - Already-annotated vars are left unchanged.
11
+
12
+ use std::collections::HashMap;
13
+ use tishlang_ast::{
14
+ ArrowBody, BinOp, CallArg, Expr, FunParam, Literal, Program, Statement, TypeAnnotation,
15
+ };
16
+
17
+ /// Scoped type environment used during inference.
18
+ #[derive(Default)]
19
+ pub struct InferCtx {
20
+ scopes: Vec<HashMap<String, TypeAnnotation>>,
21
+ }
22
+
23
+ impl InferCtx {
24
+ pub fn new() -> Self {
25
+ Self { scopes: vec![HashMap::new()] }
26
+ }
27
+
28
+ fn push_scope(&mut self) {
29
+ self.scopes.push(HashMap::new());
30
+ }
31
+
32
+ fn pop_scope(&mut self) {
33
+ self.scopes.pop();
34
+ }
35
+
36
+ fn define(&mut self, name: &str, ty: TypeAnnotation) {
37
+ if let Some(s) = self.scopes.last_mut() {
38
+ s.insert(name.to_string(), ty);
39
+ }
40
+ }
41
+
42
+ pub fn lookup(&self, name: &str) -> Option<&TypeAnnotation> {
43
+ for s in self.scopes.iter().rev() {
44
+ if let Some(t) = s.get(name) {
45
+ return Some(t);
46
+ }
47
+ }
48
+ None
49
+ }
50
+ }
51
+
52
+ fn is_number(ann: &TypeAnnotation) -> bool {
53
+ matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "number")
54
+ }
55
+
56
+ fn number_ann() -> TypeAnnotation {
57
+ TypeAnnotation::Simple("number".into())
58
+ }
59
+
60
+ fn string_ann() -> TypeAnnotation {
61
+ TypeAnnotation::Simple("string".into())
62
+ }
63
+
64
+ fn bool_ann() -> TypeAnnotation {
65
+ TypeAnnotation::Simple("boolean".into())
66
+ }
67
+
68
+ /// Infer the `TypeAnnotation` for an expression, if unambiguous.
69
+ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
70
+ match expr {
71
+ Expr::Literal { value, .. } => match value {
72
+ Literal::Number(_) => Some(number_ann()),
73
+ Literal::String(_) => Some(string_ann()),
74
+ Literal::Bool(_) => Some(bool_ann()),
75
+ Literal::Null => None,
76
+ },
77
+ Expr::Ident { name, .. } => ctx.lookup(name.as_ref()).cloned(),
78
+ Expr::Binary { left, op, right, .. } => {
79
+ let lt = infer_expr_type(left, ctx)?;
80
+ let rt = infer_expr_type(right, ctx)?;
81
+ if is_number(&lt) && is_number(&rt) {
82
+ match op {
83
+ BinOp::Add
84
+ | BinOp::Sub
85
+ | BinOp::Mul
86
+ | BinOp::Div
87
+ | BinOp::Mod
88
+ | BinOp::Pow => Some(number_ann()),
89
+ BinOp::Lt
90
+ | BinOp::Le
91
+ | BinOp::Gt
92
+ | BinOp::Ge
93
+ | BinOp::StrictEq
94
+ | BinOp::StrictNe => Some(bool_ann()),
95
+ _ => None,
96
+ }
97
+ } else {
98
+ None
99
+ }
100
+ }
101
+ Expr::Unary { op, operand, .. } => {
102
+ use tishlang_ast::UnaryOp;
103
+ match op {
104
+ UnaryOp::Neg | UnaryOp::Pos => {
105
+ let t = infer_expr_type(operand, ctx)?;
106
+ if is_number(&t) { Some(number_ann()) } else { None }
107
+ }
108
+ UnaryOp::Not => Some(bool_ann()),
109
+ _ => None,
110
+ }
111
+ }
112
+ _ => None,
113
+ }
114
+ }
115
+
116
+ /// Run inference over a program, returning a modified Program with additional
117
+ /// type annotations filled in on `VarDecl` nodes.
118
+ pub fn infer_program(program: &Program) -> Program {
119
+ let mut ctx = InferCtx::new();
120
+ Program { statements: infer_statements(&program.statements, &mut ctx) }
121
+ }
122
+
123
+ fn infer_statements(stmts: &[Statement], ctx: &mut InferCtx) -> Vec<Statement> {
124
+ stmts.iter().map(|s| infer_statement(s, ctx)).collect()
125
+ }
126
+
127
+ fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
128
+ match stmt {
129
+ Statement::VarDecl { name, mutable, type_ann, init, span } => {
130
+ // Already annotated — propagate into ctx but don't change the node.
131
+ if let Some(ann) = type_ann {
132
+ ctx.define(name.as_ref(), ann.clone());
133
+ return stmt.clone();
134
+ }
135
+ // Try to infer from init expression.
136
+ let inferred = init.as_ref().and_then(|e| infer_expr_type(e, ctx));
137
+ if let Some(ref ann) = inferred {
138
+ ctx.define(name.as_ref(), ann.clone());
139
+ }
140
+ Statement::VarDecl {
141
+ name: name.clone(),
142
+ mutable: *mutable,
143
+ type_ann: inferred,
144
+ init: init.clone(),
145
+ span: *span,
146
+ }
147
+ }
148
+ Statement::Block { statements, span } => {
149
+ ctx.push_scope();
150
+ let stmts = infer_statements(statements, ctx);
151
+ ctx.pop_scope();
152
+ Statement::Block { statements: stmts, span: *span }
153
+ }
154
+ Statement::For { init, cond, update, body, span } => {
155
+ // Scope for loop variable
156
+ ctx.push_scope();
157
+ let new_init = init.as_ref().map(|i| Box::new(infer_statement(i, ctx)));
158
+ let new_body = Box::new(infer_statement(body, ctx));
159
+ ctx.pop_scope();
160
+ Statement::For {
161
+ init: new_init,
162
+ cond: cond.clone(),
163
+ update: update.clone(),
164
+ body: new_body,
165
+ span: *span,
166
+ }
167
+ }
168
+ Statement::ForOf { name, iterable, body, span } => {
169
+ ctx.push_scope();
170
+ let new_body = Box::new(infer_statement(body, ctx));
171
+ ctx.pop_scope();
172
+ Statement::ForOf {
173
+ name: name.clone(),
174
+ iterable: iterable.clone(),
175
+ body: new_body,
176
+ span: *span,
177
+ }
178
+ }
179
+ Statement::While { cond, body, span } => {
180
+ ctx.push_scope();
181
+ let new_body = Box::new(infer_statement(body, ctx));
182
+ ctx.pop_scope();
183
+ Statement::While { cond: cond.clone(), body: new_body, span: *span }
184
+ }
185
+ Statement::DoWhile { body, cond, span } => {
186
+ ctx.push_scope();
187
+ let new_body = Box::new(infer_statement(body, ctx));
188
+ ctx.pop_scope();
189
+ Statement::DoWhile { body: new_body, cond: cond.clone(), span: *span }
190
+ }
191
+ Statement::If { cond, then_branch, else_branch, span } => {
192
+ let new_then = Box::new(infer_statement(then_branch, ctx));
193
+ let new_else = else_branch.as_ref().map(|e| Box::new(infer_statement(e, ctx)));
194
+ Statement::If {
195
+ cond: cond.clone(),
196
+ then_branch: new_then,
197
+ else_branch: new_else,
198
+ span: *span,
199
+ }
200
+ }
201
+ Statement::FunDecl { async_, name, params, rest_param, return_type, body, span } => {
202
+ ctx.push_scope();
203
+ for p in params {
204
+ if let FunParam::Simple(tp) = p {
205
+ if let Some(ann) = &tp.type_ann {
206
+ ctx.define(tp.name.as_ref(), ann.clone());
207
+ }
208
+ }
209
+ }
210
+ if let Some(rp) = rest_param {
211
+ if let Some(ann) = &rp.type_ann {
212
+ ctx.define(rp.name.as_ref(), ann.clone());
213
+ }
214
+ }
215
+ let new_body = Box::new(infer_statement(body, ctx));
216
+ ctx.pop_scope();
217
+ Statement::FunDecl {
218
+ async_: *async_,
219
+ name: name.clone(),
220
+ params: params.clone(),
221
+ rest_param: rest_param.clone(),
222
+ return_type: return_type.clone(),
223
+ body: new_body,
224
+ span: *span,
225
+ }
226
+ }
227
+ // For statements with no interesting sub-structure, clone as-is.
228
+ _ => stmt.clone(),
229
+ }
230
+ }
231
+
232
+ // Suppress unused import warning — CallArg is used indirectly via tishlang_ast.
233
+ #[allow(dead_code)]
234
+ fn _uses_call_arg(_: &CallArg) {}
235
+ #[allow(dead_code)]
236
+ fn _uses_arrow_body(_: &ArrowBody) {}
@@ -3,6 +3,7 @@
3
3
  //! Emits Rust source that links to tishlang_runtime.
4
4
 
5
5
  mod codegen;
6
+ mod infer;
6
7
  mod resolve;
7
8
  mod types;
8
9
 
@@ -25,6 +26,9 @@ mod tests {
25
26
 
26
27
  #[test]
27
28
  fn typed_assign_conversion() {
29
+ // With the inference pass and native emit, `total: number = 0` becomes f64.
30
+ // Assignment `total = total + n` (where n comes from ForOf over a Value::Array)
31
+ // emits a native f64 assignment that unboxes the Value result via from_value_expr.
28
32
  let src = r#"
29
33
  fn sum(...args: number[]): number {
30
34
  let total: number = 0
@@ -34,11 +38,16 @@ fn sum(...args: number[]): number {
34
38
  "#;
35
39
  let program = parse(src).unwrap();
36
40
  let rust = compile(&program).unwrap();
37
- assert!(rust.contains("match &_v { Value::Number(n) => *n"), "expected typed assign conversion");
41
+ // total should be declared as f64
42
+ assert!(rust.contains("let mut total: f64"), "expected total: f64");
43
+ // The return value of run() should convert total back to Value
44
+ assert!(rust.contains("Value::Number(total)"), "expected Value::Number(total) wrapping");
38
45
  }
39
46
 
40
47
  #[test]
41
48
  fn loop_var_decl_clone_outer_var() {
49
+ // With inference, outerVar = 42 gets inferred as f64. f64 is Copy, so no clone is
50
+ // needed — direct assignment is correct. The test verifies compilation succeeds.
42
51
  let src = r#"
43
52
  let outerVar = 42
44
53
  for (let i = 0; i < 5; i = i + 1) {
@@ -47,10 +56,9 @@ for (let i = 0; i < 5; i = i + 1) {
47
56
  "#;
48
57
  let program = parse(src).unwrap();
49
58
  let rust = compile(&program).unwrap();
50
- assert!(
51
- rust.contains("(outerVar).clone()"),
52
- "expected outerVar to be cloned in loop body"
53
- );
59
+ // outerVar and x are f64 (inferred) — Copy assignment, no .clone() needed.
60
+ assert!(rust.contains("let mut outerVar: f64"), "expected outerVar: f64");
61
+ assert!(rust.contains("let mut x: f64"), "expected x: f64");
54
62
  }
55
63
 
56
64
  #[test]
@@ -90,6 +98,9 @@ fn factory() {
90
98
 
91
99
  #[test]
92
100
  fn loop_var_decl_clone_via_project_full() {
101
+ // With the inference pass, `let outerVar = 42` is inferred as f64 (Copy) — no clone needed.
102
+ // This test verifies the full benchmark_granular project compiles and that outerVar
103
+ // is emitted as the inferred f64 type rather than requiring a Value clone.
93
104
  let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
94
105
  let bench = manifest.join("../../tests/core/benchmark_granular.tish").canonicalize().unwrap();
95
106
  // Use same default features as tish CLI (http, fs, process, regex)
@@ -98,9 +109,10 @@ fn factory() {
98
109
  .map(String::from)
99
110
  .collect::<Vec<_>>();
100
111
  let (rust, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
112
+ // outerVar = 42 is inferred as f64; f64 is Copy so no .clone() is emitted.
101
113
  assert!(
102
- rust.contains("(outerVar).clone()"),
103
- "expected outerVar to be cloned in benchmark_granular loop"
114
+ rust.contains("let mut outerVar: f64"),
115
+ "expected outerVar to be inferred as f64 (Copy, no clone needed)"
104
116
  );
105
117
  }
106
118
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  use std::collections::HashMap;
6
6
  use std::sync::Arc;
7
- use tishlang_ast::TypeAnnotation;
7
+ use tishlang_ast::{BinOp, TypeAnnotation};
8
8
 
9
9
  /// Concrete Rust type representation for code generation.
10
10
  #[derive(Debug, Clone, PartialEq)]
@@ -89,6 +89,38 @@ impl RustType {
89
89
  !matches!(self, RustType::Value)
90
90
  }
91
91
 
92
+ /// Check if this type is numeric (f64).
93
+ pub fn is_numeric(&self) -> bool {
94
+ matches!(self, RustType::F64)
95
+ }
96
+
97
+ /// Infer the result type of a binary operation given the operand types.
98
+ /// Returns `None` if native code cannot be emitted (fall back to Value path).
99
+ pub fn result_type_of_binop(op: BinOp, lhs: &RustType, rhs: &RustType) -> Option<RustType> {
100
+ if lhs == &RustType::F64 && rhs == &RustType::F64 {
101
+ match op {
102
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
103
+ Some(RustType::F64)
104
+ }
105
+ BinOp::Lt
106
+ | BinOp::Le
107
+ | BinOp::Gt
108
+ | BinOp::Ge
109
+ | BinOp::StrictEq
110
+ | BinOp::StrictNe => Some(RustType::Bool),
111
+ _ => None,
112
+ }
113
+ } else if lhs == &RustType::Bool && rhs == &RustType::Bool {
114
+ match op {
115
+ BinOp::And | BinOp::Or => Some(RustType::Bool),
116
+ BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
117
+ _ => None,
118
+ }
119
+ } else {
120
+ None
121
+ }
122
+ }
123
+
92
124
  /// Get the Rust type string for code generation.
93
125
  pub fn to_rust_type_str(&self) -> String {
94
126
  match self {
@@ -169,14 +201,19 @@ impl RustType {
169
201
  match self {
170
202
  RustType::Value => native_expr.to_string(),
171
203
  RustType::F64 => format!("Value::Number({})", native_expr),
172
- RustType::String => format!("Value::String({}.into())", native_expr),
204
+ RustType::String => format!("Value::String({}.clone().into())", native_expr),
173
205
  RustType::Bool => format!("Value::Bool({})", native_expr),
174
206
  RustType::Unit => "Value::Null".to_string(),
175
207
  RustType::Vec(inner) => {
176
- let inner_to_value = inner.to_value_expr("v");
208
+ // Use iter()/copied()/cloned() to avoid moving the vector.
209
+ let (iter_suffix, val_expr) = match inner.as_ref() {
210
+ RustType::F64 => (".iter().copied()", "Value::Number(v)".to_string()),
211
+ RustType::Bool => (".iter().copied()", "Value::Bool(v)".to_string()),
212
+ _ => (".iter().cloned()", inner.to_value_expr("v")),
213
+ };
177
214
  format!(
178
- "Value::Array(Rc::new(RefCell::new({}.into_iter().map(|v| {}).collect())))",
179
- native_expr, inner_to_value
215
+ "Value::Array(Rc::new(RefCell::new({}{}.map(|v| {}).collect())))",
216
+ native_expr, iter_suffix, val_expr
180
217
  )
181
218
  }
182
219
  RustType::Option(inner) => {
@@ -25,6 +25,9 @@ pub trait TishOpaque: Send + Sync {
25
25
 
26
26
  /// Get a method by name. Returns a native function if the method exists.
27
27
  fn get_method(&self, name: &str) -> Option<NativeFn>;
28
+
29
+ /// For downcasting `Arc<dyn TishOpaque>` in native crates (e.g. Polars → `DataFrame`).
30
+ fn as_any(&self) -> &dyn std::any::Any;
28
31
  }
29
32
 
30
33
  /// Trait for Promise-like values that can be awaited (block until settled).
@@ -1,6 +1,8 @@
1
- //! Bytecode to native via Cranelift.
1
+ //! Standalone native binary: embedded bytecode + VM (Cranelift used as object builder).
2
2
  //!
3
- //! Compiles Tish bytecode to native object files and links with a minimal runtime.
3
+ //! Produces an executable that runs **`tishlang_vm`** on serialized bytecode embedded in
4
+ //! the binary — not lowering of Tish opcodes to CLIF/machine code (see module docs in
5
+ //! `lower.rs`). For Rust transpile + `tishlang_runtime`, use `--native-backend rust`.
4
6
 
5
7
  mod link;
6
8
  mod lower;
@@ -25,8 +27,8 @@ impl std::fmt::Display for CraneliftError {
25
27
 
26
28
  impl std::error::Error for CraneliftError {}
27
29
 
28
- /// Compile a bytecode chunk to a native binary.
29
- /// `features` are passed to tishlang_cranelift_runtime (e.g. fs, process, http for built-in modules).
30
+ /// Build a native binary that embeds `chunk` and runs it with the bytecode VM.
31
+ /// `features` are passed to `tishlang_cranelift_runtime` (e.g. fs, process, http).
30
32
  pub fn compile_chunk_to_native(
31
33
  chunk: &Chunk,
32
34
  output_path: &Path,
@@ -1,7 +1,9 @@
1
- //! Bytecode to Cranelift IR lowering.
1
+ //! Embed serialized bytecode in an object file for the standalone native binary.
2
2
  //!
3
- //! Emits object file with tish_chunk_data and tish_chunk_len symbols.
4
- //! The link step builds a Rust binary that reads these and runs via tishlang_vm.
3
+ //! **This is not AOT compilation of Tish into Cranelift IR.** The chunk is stored as
4
+ //! read-only data (`tish_chunk_data`, `tish_chunk_len`). The link step produces an
5
+ //! executable that **deserializes the chunk and runs `tishlang_vm`** — same VM as
6
+ //! `tish run --backend vm`. Cranelift is only the object-file emitter for that blob.
5
7
 
6
8
  use std::path::Path;
7
9
 
@@ -1,6 +1,8 @@
1
- //! Runtime for Cranelift-compiled Tish programs.
1
+ //! Runtime linked into the `tish compile --native-backend cranelift` executable.
2
2
  //!
3
- //! Provides tish_run_chunk(ptr, len) which deserializes and runs bytecode.
3
+ //! **`tish_run_chunk`** deserializes embedded bytecode and runs **`tishlang_vm`** — the same
4
+ //! execution engine as `tish run --backend vm`. The crate name is historical; this is not
5
+ //! running CLIF-emitted machine code for each Tish opcode.
4
6
 
5
7
  use tishlang_bytecode::deserialize;
6
8
  use tishlang_vm::Vm;
@@ -75,6 +75,8 @@ pub struct Evaluator {
75
75
  module_cache: Rc<RefCell<HashMap<PathBuf, Value>>>,
76
76
  /// Directory of the file currently being evaluated (for resolving relative imports)
77
77
  current_dir: RefCell<Option<PathBuf>>,
78
+ /// Extra `tish:*` builtins from `TishNativeModule::virtual_builtin_modules` (shared across nested evaluators).
79
+ virtual_builtins: Rc<RefCell<HashMap<Arc<str>, Value>>>,
78
80
  }
79
81
 
80
82
  impl Evaluator {
@@ -173,6 +175,7 @@ impl Evaluator {
173
175
  scope,
174
176
  module_cache: Rc::new(RefCell::new(HashMap::new())),
175
177
  current_dir: RefCell::new(None),
178
+ virtual_builtins: Rc::new(RefCell::new(HashMap::new())),
176
179
  }
177
180
  }
178
181
 
@@ -187,6 +190,14 @@ impl Evaluator {
187
190
  }
188
191
  }
189
192
  }
193
+ {
194
+ let mut vb = eval.virtual_builtins.borrow_mut();
195
+ for module in modules {
196
+ for (spec, value) in module.virtual_builtin_modules() {
197
+ vb.insert(Arc::from(spec), value);
198
+ }
199
+ }
200
+ }
190
201
  eval
191
202
  }
192
203
 
@@ -502,7 +513,7 @@ impl Evaluator {
502
513
  /// Load and evaluate a module, returning its exports object. Uses cache.
503
514
  fn load_module(&mut self, from: &str) -> Result<Value, EvalError> {
504
515
  if from.starts_with("tish:") {
505
- return Self::load_builtin_module(from);
516
+ return self.load_builtin_module(from);
506
517
  }
507
518
  let dir = self.current_dir.borrow().clone().ok_or_else(|| {
508
519
  EvalError::Error("Cannot resolve imports: no current file directory (use run_file)".to_string())
@@ -582,8 +593,11 @@ impl Evaluator {
582
593
  Ok(path)
583
594
  }
584
595
 
585
- /// Load built-in module (tish:fs, tish:http, tish:process). Features auto-enabled when imported.
586
- fn load_builtin_module(spec: &str) -> Result<Value, EvalError> {
596
+ /// Load built-in module (tish:fs, tish:http, tish:process, …) or a virtual module from native crates.
597
+ fn load_builtin_module(&self, spec: &str) -> Result<Value, EvalError> {
598
+ if let Some(v) = self.virtual_builtins.borrow().get(spec) {
599
+ return Ok(v.clone());
600
+ }
587
601
  match spec {
588
602
  "tish:fs" => {
589
603
  #[cfg(feature = "fs")]
@@ -675,15 +689,15 @@ impl Evaluator {
675
689
  }
676
690
  _ => {
677
691
  return Err(EvalError::Error(format!(
678
- "Unknown built-in module: {}. Supported: tish:fs, tish:http, tish:process, tish:ws",
692
+ "Unknown built-in module: {}. Supported: tish:fs, tish:http, tish:process, tish:ws (plus any registered by native modules)",
679
693
  spec
680
694
  )));
681
695
  }
682
696
  }
683
697
  }
684
698
 
685
- fn load_builtin_export(spec: &str, export_name: &str) -> Result<Value, EvalError> {
686
- let module = Self::load_builtin_module(spec)?;
699
+ fn load_builtin_export(&self, spec: &str, export_name: &str) -> Result<Value, EvalError> {
700
+ let module = self.load_builtin_module(spec)?;
687
701
  let exports = match &module {
688
702
  Value::Object(m) => m.borrow().clone(),
689
703
  _ => return Err(EvalError::Error("Built-in module must be object".into())),
@@ -1582,7 +1596,7 @@ impl Evaluator {
1582
1596
  "JSX is not supported in the interpreter. Use 'tish compile --target js' to compile to JavaScript.".to_string(),
1583
1597
  )),
1584
1598
  Expr::NativeModuleLoad { spec, export_name, .. } => {
1585
- Self::load_builtin_export(spec.as_ref(), export_name.as_ref())
1599
+ self.load_builtin_export(spec.as_ref(), export_name.as_ref())
1586
1600
  }
1587
1601
  Expr::TypeOf { operand, .. } => {
1588
1602
  let v = self.eval_expr(operand)?;
@@ -2015,6 +2029,7 @@ impl Evaluator {
2015
2029
  scope: Rc::clone(scope),
2016
2030
  module_cache: Rc::clone(&self.module_cache),
2017
2031
  current_dir: RefCell::new(self.current_dir.borrow().clone()),
2032
+ virtual_builtins: Rc::clone(&self.virtual_builtins),
2018
2033
  };
2019
2034
  match eval.eval_statement(body) {
2020
2035
  Ok(v) => Ok(v),
@@ -2259,6 +2274,7 @@ impl Evaluator {
2259
2274
  scope,
2260
2275
  module_cache: Rc::clone(&self.module_cache),
2261
2276
  current_dir: RefCell::new(self.current_dir.borrow().clone()),
2277
+ virtual_builtins: Rc::clone(&self.virtual_builtins),
2262
2278
  };
2263
2279
  match eval.eval_statement(body) {
2264
2280
  Ok(v) => Ok(v),
@@ -14,6 +14,7 @@ pub mod regex;
14
14
  mod value;
15
15
 
16
16
  pub use eval::Evaluator;
17
+ pub use value::PropMap;
17
18
  pub use value::Value;
18
19
 
19
20
  /// Trait for pluggable native modules (e.g. Polars). Implement to register
@@ -21,6 +22,12 @@ pub use value::Value;
21
22
  pub trait TishNativeModule: Send + Sync {
22
23
  fn name(&self) -> &'static str;
23
24
  fn register(&self) -> std::collections::HashMap<std::sync::Arc<str>, Value>;
25
+
26
+ /// Virtual `tish:*` modules for `import { x } from 'tish:…'` (e.g. `tish:polars`).
27
+ /// Return `(specifier, exports_object)` pairs. Default: none.
28
+ fn virtual_builtin_modules(&self) -> Vec<(&'static str, Value)> {
29
+ vec![]
30
+ }
24
31
  }
25
32
  #[cfg(feature = "regex")]
26
33
  pub use regex::TishRegExp;
@@ -1,8 +1,8 @@
1
- //! LLVM backend for Tish.
1
+ //! LLVM/clang link path for the **embedded-bytecode + VM** native binary.
2
2
  //!
3
- //! Compiles Tish bytecode to native binary using clang (LLVM toolchain).
4
- //! Emits a C file with embedded bytecode, compiles with clang to object code,
5
- //! then links with tishlang_cranelift_runtime.
3
+ //! Emits a C file that only holds the serialized chunk as bytes; clang produces `chunk.o`,
4
+ //! then links with **`tishlang_cranelift_runtime`** (same `tish_run_chunk` + `tishlang_vm`
5
+ //! entry as `--native-backend cranelift`). This is **not** LLVM IR lowering of Tish opcodes.
6
6
 
7
7
  use std::fs;
8
8
  use std::path::Path;
@@ -1,14 +1,13 @@
1
1
  //! Native code generation backend for Tish.
2
2
  //!
3
- //! Target architecture (per plan):
4
- //! - Phase 2: Bytecode -> Cranelift IR -> .o -> link with minimal runtime
5
- //! - Current: Delegates to tishlang_compile (Rust codegen) + cargo build as interim path
3
+ //! - **`rust`:** `tishlang_compile` emits Rust calling **`tishlang_runtime`** (`Value`, etc.),
4
+ //! then `cargo build --release` links the user binary.
5
+ //! - **`cranelift`:** Embeds serialized bytecode in an object file and links **`tishlang_cranelift_runtime`**
6
+ //! — the executable runs **`tishlang_vm`** on that chunk (same as `tish run --backend vm`), not CLIF lowering.
7
+ //! - **`llvm`:** Same embedded-bytecode + VM link path via `tishlang_llvm` / shared linker.
6
8
  //!
7
- //! Once Cranelift backend is implemented, this crate will:
8
- //! 1. Take Chunk (bytecode) as input
9
- //! 2. Lower to Cranelift IR
10
- //! 3. Emit .o via cranelift-object
11
- //! 4. Link against prebuilt tishlang_runtime staticlib
9
+ //! **Future:** Lower bytecode (or typed IR) through Cranelift/LLVM to real machine code where semantics allow;
10
+ //! emit Rust using `Vec<f64>` / fixed primitives instead of `Value` on hot paths.
12
11
 
13
12
  mod build;
14
13
 
@@ -31,9 +30,9 @@ impl std::error::Error for NativeError {}
31
30
 
32
31
  /// Compile a Tish project to a native binary.
33
32
  ///
34
- /// - `native_backend == "rust"`: Full Rust codegen + cargo build (supports native imports).
35
- /// - `native_backend == "cranelift"`: Bytecode -> Cranelift -> native (pure Tish only).
36
- /// - `native_backend == "llvm"`: Experimental LLVM backend (not implemented yet).
33
+ /// - `native_backend == "rust"`: Rust source + `tishlang_runtime` + cargo (native imports).
34
+ /// - `native_backend == "cranelift"`: Embedded bytecode + VM binary (pure Tish only); not opcode AOT yet.
35
+ /// - `native_backend == "llvm"`: Embedded bytecode + VM via LLVM/clang link path.
37
36
  pub fn compile_to_native(
38
37
  entry_path: &Path,
39
38
  project_root: Option<&Path>,
@@ -627,6 +627,12 @@ impl<'a> Parser<'a> {
627
627
  span: self.span_end(span_start),
628
628
  });
629
629
  }
630
+ let type_ann = if matches!(self.peek_kind(), Some(TokenKind::Colon)) {
631
+ self.advance();
632
+ Some(self.parse_type_annotation()?)
633
+ } else {
634
+ None
635
+ };
630
636
  let init_expr = if matches!(self.peek_kind(), Some(TokenKind::Assign)) {
631
637
  self.advance();
632
638
  Some(self.parse_expr()?)
@@ -639,7 +645,7 @@ impl<'a> Parser<'a> {
639
645
  Some(Box::new(Statement::VarDecl {
640
646
  name,
641
647
  mutable,
642
- type_ann: None, // For-loop variables don't have type annotations (yet)
648
+ type_ann,
643
649
  init: init_expr,
644
650
  span: self.span_end(var_span_start),
645
651
  }))
@@ -211,6 +211,10 @@ impl TishOpaque for HttpReadableStream {
211
211
  "ReadableStream"
212
212
  }
213
213
 
214
+ fn as_any(&self) -> &dyn std::any::Any {
215
+ self
216
+ }
217
+
214
218
  fn get_method(&self, name: &str) -> Option<NativeFn> {
215
219
  if name != "getReader" {
216
220
  return None;
@@ -247,6 +251,10 @@ impl TishOpaque for HttpStreamReader {
247
251
  "ReadableStreamDefaultReader"
248
252
  }
249
253
 
254
+ fn as_any(&self) -> &dyn std::any::Any {
255
+ self
256
+ }
257
+
250
258
  fn get_method(&self, name: &str) -> Option<NativeFn> {
251
259
  if name != "read" {
252
260
  return None;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "1.0.33",
3
+ "version": "1.0.34",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, compile to native.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file