Version: 0.9.0 Status: Draft (Program Hooks & Verbatim Blocks Added) Target Platforms: Babel (JavaScript) & SWC (Rust/WASM)
RustScript is a domain-specific language for writing AST transformation plugins that compile to both Babel (JavaScript) and SWC (Rust). It enforces a strict visitor pattern with explicit ownership semantics that map cleanly to both garbage-collected and borrow-checked runtimes.
- Strict Visitor, Loose Context: Enforces Rust-like
VisitMutpattern - Immutable by Default: All mutations must be explicit
- No Path Magic: No implicit parent traversal; state must be tracked explicitly
- Unified AST: Subset of nodes common to ESTree (Babel) and swc_ecma_ast
- Clone-to-Own: Explicit
.clone()required for value extraction
RustScript finds the intersection of JavaScript and Rust capabilities, not the union. Code that compiles must be semantically valid in both targets.
plugin fn let const if else
match return true false null for
in while break continue struct enum
impl use pub mut self Self
traverse using writer where
async await trait type as loop
mod crate super dyn static
// Arithmetic
+ - * / %
// Comparison
== != < > <= >=
// Logical
&& || !
// Assignment
= += -= *= /=
// Reference/Dereference
& *
// Member Access
. :: ?.
// Special
=> -> .. ... ?
// Delimiters
| (for closures)
// Single-line comment
/*
Multi-line comment
*/
/// Documentation comment (preserved in output)
// Strings (always use Str type internally)
"hello world"
"escaped \"quotes\""
// Numbers
42
3.14
0xFF
0b1010
// Booleans
true
false
// Null
null
// Unit (empty value)
()
| RustScript | Babel (JS) | SWC (Rust) |
|---|---|---|
Str |
string |
JsWord / Atom |
i32 |
number |
i32 |
f64 |
number |
f64 |
bool |
boolean |
bool |
() |
undefined |
() |
&T // Immutable reference
&mut T // Mutable reference
Compilation:
- Babel: References are ignored; values passed directly
- SWC: References preserved as-is
Vec<T> // Dynamic array
Option<T> // Optional value (Some/None)
HashMap<K, V> // Key-value map
HashSet<T> // Unique set
Compilation:
| RustScript | Babel (JS) | SWC (Rust) |
|---|---|---|
Vec<T> |
Array |
Vec<T> |
Option<T> |
T | null |
Option<T> |
HashMap<K,V> |
Map or Object |
HashMap<K,V> |
HashSet<T> |
Set |
HashSet<T> |
(T1, T2) // Two-element tuple
(T1, T2, T3) // Three-element tuple
() // Unit type (zero-element tuple)
Usage:
// Tuple types in function signatures
fn get_coords() -> (i32, i32) {
(10, 20)
}
// Tuple destructuring
let (x, y) = get_coords();
// As Result type parameter
fn validate() -> Result<(), Str> {
Ok(())
}
Compilation:
- Babel: Tuples become arrays
[T1, T2]; unit()becomesundefined - SWC: Tuples preserved as
(T1, T2); unit as()
For generating code (transpiler use case):
// A buffer that handles platform-specific string concatenation
type CodeBuilder;
impl CodeBuilder {
fn new() -> CodeBuilder;
fn append(s: Str);
fn newline();
fn indent();
fn dedent();
fn to_string() -> Str;
}
Compilation:
- Babel: Uses array-based string building with
.join() - SWC: Uses
Stringwithpush_str()
See Section 7 (Unified AST) for complete mapping.
Identifier
CallExpression
MemberExpression
BinaryExpression
// ... etc
Every RustScript file must declare a plugin:
plugin MyTransformer {
// visitor methods and helpers
}
Compiles to:
// Babel
module.exports = function({ types: t }) {
return {
visitor: { /* ... */ }
};
};// SWC
pub struct MyTransformer;
impl VisitMut for MyTransformer { /* ... */ }fn function_name(param: Type, param2: &mut Type) -> ReturnType {
// body
}
Visibility:
pub fn public_function() { } // Exported
fn private_function() { } // Internal
Generic functions allow type parameters and trait bounds using Rust syntax:
// Simple generic function
fn identity<T>(value: T) -> T {
value
}
// Generic with trait bound (where clause)
pub fn map_expression<F>(expr: &Expression, mapper: F) -> Str
where
F: Fn(&Expression, bool) -> Str
{
mapper(expr, false)
}
// Multiple type parameters and constraints
fn transform<F, G>(input: Str, f: F, g: G) -> Str
where
F: Fn(Str) -> Str,
G: Fn(Str) -> Str
{
g(f(input))
}
Compilation Model:
Generic functions use syntactic passthrough - the parser recognizes the syntax, but generics are treated as metadata for code generation rather than enforced by a constraint solver.
// Babel: Generics are stripped entirely
export function mapExpression(expr, mapper) {
return mapper(expr, false);
}// SWC: Generics preserved exactly
pub fn map_expression<F>(expr: &Expr, mapper: F) -> String
where
F: Fn(&Expr, bool) -> String
{
mapper(expr, false)
}Supported Syntax:
- Type parameters:
<F, T, U> - Where clauses:
where F: Fn(...) -> R - Function trait types:
Fn(T1, T2) -> R - Reference parameters in traits:
Fn(&Type, bool) -> Str
Limitations:
- No generic constraint checking (Marathon philosophy: syntax without semantic solver)
- Type parameters are not tracked in the type system
- Generics only work on functions, not on structs or enums
let name = value; // Immutable binding
let mut name = value; // Mutable binding
const NAME = value; // Compile-time constant
Clone Requirement:
// Extracting from a reference requires explicit clone
let name = node.name.clone(); // Required
let name = node.name; // ERROR: implicit borrow
Compilation:
// Babel: .clone() is stripped
const name = node.name;// SWC: .clone() preserved
let name = node.name.clone();struct ComponentInfo {
name: Str,
props: Vec<Prop>,
has_state: bool,
}
Compiles to:
// Babel: Plain object shape (documentation only)
// Runtime: { name: string, props: Array, has_state: boolean }// SWC
#[derive(Clone, Debug)]
pub struct ComponentInfo {
pub name: String,
pub props: Vec<Prop>,
pub has_state: bool,
}enum HookType {
State,
Effect,
Ref,
Custom(Str),
}
Compiles to:
// Babel: Tagged union pattern
// { type: "State" } | { type: "Effect" } | { type: "Custom", value: string }// SWC
pub enum HookType {
State,
Effect,
Ref,
Custom(String),
}For multi-file projects, create standalone modules without plugin or writer declarations:
// File: utils/helpers.rsc
// Public function (exported)
pub fn get_component_name(node: &FunctionDeclaration) -> Str {
node.id.name.clone()
}
// Private function (not exported)
fn internal_helper() -> Str {
"internal"
}
// Public struct (exported)
pub struct ComponentInfo {
pub name: Str,
pub props: Vec<Str>,
}
Compiles to:
// Babel (CommonJS)
function getComponentName(node) {
return node.id.name;
}
function internalHelper() {
return "internal";
}
class ComponentInfo {
constructor(name, props) {
this.name = name;
this.props = props;
}
}
module.exports = {
getComponentName,
ComponentInfo,
};// SWC (Rust module)
//! Generated by RustScript compiler
//! Do not edit manually
#[derive(Debug, Clone)]
pub struct ComponentInfo {
pub name: String,
pub props: Vec<String>,
}
pub fn get_component_name(node: &FnDecl) -> String {
node.ident.sym.to_string()
}
fn internal_helper() -> String {
"internal".to_string()
}Import functionality from other modules:
// Import from file module
use "./utils/helpers.rsc";
// Import specific items
use "./utils/helpers.rsc" { get_component_name, ComponentInfo };
// Import with alias
use "./utils/helpers.rsc" as helpers;
// Import built-in modules
use fs;
use json;
use path;
Module Resolution:
- Relative paths (starting with
./or../): File-based modules - Built-in names (no path): Standard library modules (
fs,json,path) - Extensions:
.rscextension is optional
Compiles to:
// Babel
const helpers = require('./utils/helpers.js');
const { getComponentName, ComponentInfo } = require('./utils/helpers.js');
const fs = require('fs');
const path = require('path');// SWC
mod utils {
pub mod helpers;
}
use utils::helpers;
use utils::helpers::{get_component_name, ComponentInfo};
use std::fs;
use std::path;RustScript supports plugin lifecycle hooks that run before and after the visitor traversal:
plugin MinimactPlugin {
/// Pre-hook: runs before any visitors
fn pre(file: &File) {
// Save original code before React transforms JSX
file.metadata.originalCode = file.code;
}
fn visit_jsx_element(node: &mut JSXElement, ctx: &Context) {
// Regular visitor method
}
/// Exit hook: runs after all visitors complete
fn exit(program: &mut Program, state: &PluginState) {
// Post-processing after all transformations
generate_metadata_file(state);
}
}
Compiles to:
// Babel
module.exports = function({ types: t }) {
// Pre-hook function
function pre(file) {
file.metadata.originalCode = file.code;
}
// Exit hook function
function exit(program, state) {
generate_metadata_file(state);
}
return {
pre(file) {
pre(file);
},
visitor: {
Program: {
exit(path, state) {
exit(path.node, state);
}
},
JSXElement(path) {
// ...
}
}
};
};// SWC: Hooks are not supported in SWC visitor pattern
// exit() becomes a regular method, pre() is omitted
impl VisitMut for MinimactPlugin {
fn visit_mut_jsx_element(&mut self, n: &mut JSXElement) {
// ...
}
}Use Cases:
- pre(): Save original source code for format-preserving transformations (Recast)
- exit(): Generate output files, collect metadata, perform final processing
Limitations:
pre()only available in Babel (no SWC equivalent)exit()becomes a regular method in SWC, not automatically called- Hooks receive different parameters than visitor methods
For platform-specific operations (like Recast in Babel or custom Rust code in SWC), RustScript provides verbatim blocks that emit raw code:
plugin RecastPlugin {
fn pre(file: &File) {
babel! {
// Raw JavaScript - preserved exactly as written
file.metadata.originalCode = file.code;
}
}
fn visit_jsx_element(node: &mut JSXElement, ctx: &Context) {
// Mix RustScript with verbatim blocks
let tag_name = &node.opening_element.name;
babel! {
// Babel-specific: Use Recast for format-preserving edits
const recast = require('recast');
const keyAttr = t.jsxAttribute(
t.jsxIdentifier('key'),
t.stringLiteral('generated-key')
);
node.openingElement.attributes.push(keyAttr);
}
swc! {
// SWC-specific: Rust AST manipulation
node.opening.attrs.push(JSXAttr {
span: DUMMY_SP,
name: JSXAttrName::Ident(Ident::new("key".into(), DUMMY_SP)),
value: Some(JSXAttrValue::Lit(Lit::Str("generated-key".into())))
});
}
}
fn exit(program: &mut Program, state: &PluginState) {
babel! {
// Use Recast to generate .keys file with preserved formatting
const recast = require('recast');
const originalAst = recast.parse(state.file.metadata.originalCode, {
parser: require('recast/parsers/babel-ts')
});
// Traverse and modify...
const output = recast.print(originalAst, {
tabWidth: 2,
quote: 'single'
});
fs.writeFileSync(keysFilePath, output.code);
}
}
}
Syntax:
babel! { ... }orjs! { ... }- JavaScript code (Babel only)swc! { ... }orrust! { ... }- Rust code (SWC only)
Compiles to:
// Babel: babel!/js! blocks are emitted, swc!/rust! blocks are omitted
function visit_jsx_element(node, ctx) {
const tag_name = node.opening_element.name;
// Verbatim JavaScript
const recast = require('recast');
const keyAttr = t.jsxAttribute(
t.jsxIdentifier('key'),
t.stringLiteral('generated-key')
);
node.openingElement.attributes.push(keyAttr);
/* SWC-only code omitted */
}// SWC: swc!/rust! blocks are emitted, babel!/js! blocks are omitted
fn visit_mut_jsx_element(&mut self, n: &mut JSXElement) {
let tag_name = &n.opening_element.name;
// Babel-only code omitted
// Verbatim Rust
node.opening.attrs.push(JSXAttr {
span: DUMMY_SP,
name: JSXAttrName::Ident(Ident::new("key".into(), DUMMY_SP)),
value: Some(JSXAttrValue::Lit(Lit::Str("generated-key".into())))
});
}Characteristics:
- Format-preserving: Whitespace and formatting inside verbatim blocks is preserved exactly
- No type checking: Contents are opaque to RustScript's semantic analysis
- Platform-specific: Each block targets one platform, other platform sees comment
- Token-based extraction: Uses token spans to extract raw source between braces
Use Cases:
- Recast integration for format-preserving Babel transformations
- Platform-specific npm package imports (
require('recast')) - Direct manipulation of Babel or SWC AST types not in the unified AST
- File I/O operations (
fs.writeFileSync) - Complex JavaScript patterns (object literal configs, etc.)
Design Philosophy:
Verbatim blocks embody the "surgical integration" principle: RustScript handles cross-platform AST transformations, verbatim blocks handle platform-specific edge cases. This provides maximum flexibility with minimum abstraction cost.
RustScript supports organizing code across multiple files:
my-plugin/
main.rsc # Entry point (plugin declaration)
utils/
helpers.rsc # Helper functions
types.rsc # Type definitions
extractors/
props.rsc # Prop extraction
hooks.rsc # Hook extraction
Plugin/Writer Modules (Entry Points):
- Contain
pluginorwriterdeclaration - Main file that orchestrates the transformation
- Can import from other modules
Library Modules:
- Pure function/struct/enum definitions
- Use
pubto export items - No
pluginorwriterdeclaration
Only items marked with pub are exported:
// Exported (visible to importers)
pub fn public_function() { }
pub struct PublicStruct { }
pub enum PublicEnum { }
// Not exported (module-private)
fn private_function() { }
struct PrivateStruct { }
enum PrivateEnum { }
// Import all exports (use with module prefix)
use "./helpers.rsc" as h;
let name = h::get_name(node);
// Import specific items (use directly)
use "./helpers.rsc" { get_name };
let name = get_name(node);
// Import multiple items
use "./helpers.rsc" { get_name, escape_string, ComponentInfo };
RustScript provides standard library modules:
- fs - File system operations (read, write, exists)
- json - JSON serialization and deserialization
- path - Path manipulation utilities
- parser - Runtime AST parsing (Code → AST)
- codegen - AST to code conversion (AST → Code)
use fs;
// Read file
let content = fs::read_file("input.txt")?;
// Write file
fs::write_file("output.txt", content)?;
// Check if file exists
if fs::exists("config.json") {
// ...
}
// Read directory
let files = fs::read_dir("./src")?;
Babel Compilation:
const fs = require('fs');
const content = fs.readFileSync("input.txt", "utf-8");
fs.writeFileSync("output.txt", content);
const exists = fs.existsSync("config.json");
const files = fs.readdirSync("./src");SWC Compilation:
use std::fs;
let content = fs::read_to_string("input.txt")?;
fs::write("output.txt", content)?;
let exists = std::path::Path::new("config.json").exists();
let files = fs::read_dir("./src")?;use json;
// Serialize to JSON
let json_str = json::stringify(data);
// Parse from JSON
let data = json::parse(json_str)?;
Babel Compilation:
const jsonStr = JSON.stringify(data, null, 2);
const data = JSON.parse(jsonStr);SWC Compilation:
let json_str = serde_json::to_string_pretty(&data)?;
let data = serde_json::from_str(&json_str)?;use path;
// Join paths
let full_path = path::join(vec!["src", "utils", "helpers.rsc"]);
// Get directory name
let dir = path::dirname("/src/utils/helpers.rsc");
// Get filename
let name = path::basename("/src/utils/helpers.rsc");
// Get extension
let ext = path::extname("/src/utils/helpers.rsc");
Babel Compilation:
const path = require('path');
const fullPath = path.join("src", "utils", "helpers.rsc");
const dir = path.dirname("/src/utils/helpers.rsc");
const name = path.basename("/src/utils/helpers.rsc");
const ext = path.extname("/src/utils/helpers.rsc");SWC Compilation:
use std::path::{Path, PathBuf};
let full_path = PathBuf::from("src").join("utils").join("helpers.rsc");
let dir = Path::new("/src/utils/helpers.rsc").parent();
let name = Path::new("/src/utils/helpers.rsc").file_name();
let ext = Path::new("/src/utils/helpers.rsc").extension();The parser module provides dynamic parsing capabilities for analyzing imported files at runtime. This enables cross-file analysis, such as inspecting custom hooks or components from external files.
use parser;
// Parse a TypeScript/JavaScript file
let ast = parser::parse_file("./useCounter.tsx")?;
// Parse code from a string
let code = "function foo() { return 42; }";
let ast = parser::parse(code)?;
// Parse with specific syntax
let ast = parser::parse_with_syntax(code, "TypeScript")?;
// Analyze the parsed AST
for stmt in &ast.body {
if let Statement::FunctionDeclaration(ref func) = stmt {
// Process function...
}
}
Babel Compilation:
const babel = require('@babel/core');
const fs = require('fs');
const parser = {
parse_file: (path) => {
try {
const code = fs.readFileSync(path, 'utf-8');
const ast = babel.parseSync(code, {
filename: path,
presets: ['@babel/preset-typescript'],
plugins: ['@babel/plugin-syntax-jsx'],
});
return { ok: true, value: ast };
} catch (error) {
return { ok: false, error: error.message };
}
},
parse: (code) => {
try {
const ast = babel.parseSync(code, {
presets: ['@babel/preset-typescript'],
plugins: ['@babel/plugin-syntax-jsx'],
});
return { ok: true, value: ast };
} catch (error) {
return { ok: false, error: error.message };
}
},
parse_with_syntax: (code, syntax) => {
try {
let options = {};
if (syntax === 'TypeScript') {
options = {
presets: ['@babel/preset-typescript'],
plugins: ['@babel/plugin-syntax-jsx'],
};
} else if (syntax === 'JSX') {
options = {
plugins: ['@babel/plugin-syntax-jsx'],
};
}
const ast = babel.parseSync(code, options);
return { ok: true, value: ast };
} catch (error) {
return { ok: false, error: error.message };
}
},
};SWC Compilation:
use swc_common::{FileName, SourceMap};
use swc_ecma_parser::{Parser, StringInput, Syntax, TsConfig, EsConfig};
use std::sync::Arc;
mod parser {
use super::*;
pub fn parse_file(path: &str) -> Result<Program, String> {
let source_map = Arc::new(SourceMap::default());
let code = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read file: {}", e))?;
let file = source_map.new_source_file(
FileName::Real(path.into()),
code,
);
let syntax = Syntax::Typescript(TsConfig {
tsx: true,
decorators: false,
..Default::default()
});
let mut parser = Parser::new(syntax, StringInput::from(&*file), None);
parser.parse_program()
.map_err(|e| format!("Parse error: {:?}", e))
}
pub fn parse(code: &str) -> Result<Program, String> {
let source_map = Arc::new(SourceMap::default());
let file = source_map.new_source_file(
FileName::Anon,
code.to_string(),
);
let syntax = Syntax::Typescript(TsConfig {
tsx: true,
decorators: false,
..Default::default()
});
let mut parser = Parser::new(syntax, StringInput::from(&*file), None);
parser.parse_program()
.map_err(|e| format!("Parse error: {:?}", e))
}
pub fn parse_with_syntax(code: &str, syntax_type: &str) -> Result<Program, String> {
let source_map = Arc::new(SourceMap::default());
let file = source_map.new_source_file(
FileName::Anon,
code.to_string(),
);
let syntax = match syntax_type {
"TypeScript" => Syntax::Typescript(TsConfig {
tsx: true,
decorators: false,
..Default::default()
}),
"JSX" => Syntax::Es(EsConfig {
jsx: true,
..Default::default()
}),
_ => Syntax::Es(EsConfig::default()),
};
let mut parser = Parser::new(syntax, StringInput::from(&*file), None);
parser.parse_program()
.map_err(|e| format!("Parse error: {:?}", e))
}
}Notes:
- All parser functions return
Result<Program, String>for error handling - Use the
?operator for automatic error propagation - The parser module is designed for read-only AST analysis (no modifications)
- See rustscript-parser-module.md for detailed documentation and usage examples
The codegen module provides the inverse of parsing: converting AST nodes back to source code strings. This is useful for code generation, templating, debugging, and extracting code snippets.
use codegen;
// Convert any AST node to source code
let code = codegen::generate(expr);
// With formatting options
let options = CodegenOptions {
minified: true,
quotes: QuoteStyle::Single,
};
let code = codegen::generate_with_options(expr, options);
Babel Compilation:
const generate = require('@babel/generator').default;
// Simple generation
const code = generate(expr).code;
// With options
const code = generate(expr, {
minified: true,
quotes: "single"
}).code;SWC Compilation:
use swc_ecma_codegen::{text_writer::JsWriter, Emitter, Config as CodegenConfig};
use swc_common::SourceMap;
use std::sync::Arc;
// Helper functions are generated automatically
fn codegen_to_string<N: swc_ecma_visit::Node>(node: &N) -> String {
let mut buf = vec![];
{
let cm = Arc::new(SourceMap::default());
let mut emitter = Emitter {
cfg: CodegenConfig::default(),
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
};
node.emit_with(&mut emitter).unwrap();
}
String::from_utf8(buf).unwrap()
}
fn codegen_to_string_with_config<N: swc_ecma_visit::Node>(node: &N, cfg: CodegenConfig) -> String {
let mut buf = vec![];
{
let cm = Arc::new(SourceMap::default());
let mut emitter = Emitter {
cfg,
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
};
node.emit_with(&mut emitter).unwrap();
}
String::from_utf8(buf).unwrap()
}
// Usage
let code = codegen_to_string(expr);CodegenOptions Struct:
struct CodegenOptions {
minified: bool, // Minify output (remove unnecessary whitespace)
compact: bool, // Compact output (some whitespace)
quotes: QuoteStyle, // Single or Double quotes
semicolons: bool, // Include semicolons
}
enum QuoteStyle {
Single,
Double,
}
Use Cases:
- Code Extraction - Save generated code snippets:
fn extract_function_code(func: &FunctionDeclaration) -> Str {
codegen::generate(func)
}
- Template Generation - Build code dynamically:
fn generate_wrapper(inner_expr: &Expr) -> Str {
let wrapper = build_wrapper_node(inner_expr);
codegen::generate(wrapper)
}
- Debug Logging - Readable AST output:
fn debug_expr(expr: &Expr) {
let code = codegen::generate(expr);
println!("Expression: {}", code);
}
- Code Comparison - Normalize and compare:
fn expressions_equal(a: &Expr, b: &Expr) -> bool {
let code_a = codegen::generate(a);
let code_b = codegen::generate(b);
code_a == code_b
}
Notes:
- The codegen module generates syntactically valid JavaScript/TypeScript code
- Formatting may differ between Babel and SWC targets
- Comments and source maps are not preserved in the generated code
- This is a one-way operation (Code → AST is parsing, AST → Code is codegen)
- See rustscript-codegen-module.md for detailed documentation and usage examples
main.rsc (Entry point):
use "./utils/helpers.rsc" { get_component_name };
use "./extractors/props.rsc" { extract_props };
use fs;
use json;
plugin ReactAnalyzer {
struct State {
components: Vec<ComponentInfo>,
}
struct ComponentInfo {
name: Str,
props: Vec<PropInfo>,
}
fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
let name = get_component_name(node);
let props = extract_props(node);
self.state.components.push(ComponentInfo {
name,
props,
});
}
fn visit_program_exit(node: &Program, ctx: &Context) {
for component in &self.state.components {
let json_data = json::stringify(component);
let filename = format!("{}.meta.json", component.name);
fs::write_file(filename, json_data)?;
}
}
}
utils/helpers.rsc:
pub fn get_component_name(node: &FunctionDeclaration) -> Str {
node.id.name.clone()
}
pub fn is_component(name: &Str) -> bool {
let first = name.chars().next();
if let Some(c) = first {
c.is_uppercase()
} else {
false
}
}
extractors/props.rsc:
pub struct PropInfo {
pub name: Str,
pub type_name: Str,
}
pub fn extract_props(node: &FunctionDeclaration) -> Vec<PropInfo> {
let mut props = vec![];
if node.params.len() > 0 {
// Extract from first parameter
// ... extraction logic ...
}
props
}
plugin MyPlugin {
fn visit_<node_type>(node: &mut NodeType, ctx: &Context) {
// transformation logic
}
}
Naming Convention:
visit_identifier→ visitsIdentifiernodesvisit_call_expression→ visitsCallExpressionnodes- Snake_case function name maps to PascalCase node type
The Context provides limited scope analysis available in both engines:
ctx.scope // Current scope information
ctx.filename // Source filename
ctx.generate_uid(hint) // Generate unique identifier
Direct assignment to the visitor argument triggers replacement:
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
// This is "statement lowering" - not regular assignment
*node = CallExpression {
callee: Identifier::new("newName"),
arguments: vec![],
};
}
Compiles to:
// Babel: Lowered to path.replaceWith()
path.replaceWith(t.callExpression(
t.identifier("newName"),
[]
));// SWC: Direct assignment
*node = CallExpr {
callee: Callee::Expr(Box::new(Expr::Ident(Ident::new("newName".into(), DUMMY_SP)))),
args: vec![],
..Default::default()
};In Babel, this is path.remove(). In SWC, this requires replacing with a NoOp or filtering. RustScript standardizes this:
fn visit_expression_statement(node: &mut ExpressionStatement, ctx: &Context) {
if should_remove(node) {
*node = Statement::noop(); // Compiles to path.remove() or Stmt::Empty
}
}
Compiles to:
// Babel
if (shouldRemove(node)) {
path.remove();
}// SWC
if should_remove(node) {
*node = Stmt::Empty(EmptyStmt { span: DUMMY_SP });
}Inserting siblings is difficult in SWC's VisitMut. RustScript solves this by exposing a flat_map_in_place helper:
fn visit_block_statement(node: &mut BlockStatement, ctx: &Context) {
// "stmts" is a special view of the block's body
node.stmts.flat_map_in_place(|stmt| {
if stmt.is_return() {
// Replace 1 statement with 2 (Injection)
vec![
Statement::expression(log_call()),
stmt.clone()
]
} else {
// Keep as-is
vec![stmt.clone()]
}
});
}
Compiles to:
// Babel: Iterates path.get('body') and uses path.replaceWithMultiple()
const body = path.get('body');
const newBody = [];
for (const stmt of body) {
if (t.isReturnStatement(stmt.node)) {
newBody.push(t.expressionStatement(logCall()));
newBody.push(stmt.node);
} else {
newBody.push(stmt.node);
}
}
path.node.body = newBody;// SWC: Uses flat_map inside visit_mut_block_stmt
let new_stmts: Vec<Stmt> = node.stmts.iter().flat_map(|stmt| {
if matches!(stmt, Stmt::Return(_)) {
vec![
Stmt::Expr(ExprStmt { expr: Box::new(log_call()), span: DUMMY_SP }),
stmt.clone()
]
} else {
vec![stmt.clone()]
}
}).collect();
node.stmts = new_stmts;Direct property mutation is NOT allowed:
// ERROR: Cannot mutate properties directly
node.name = "newName";
Rationale: Babel's scope tracker may not be notified. Always replace the whole node or use clone-and-rebuild pattern.
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
// Process children first (post-order)
node.visit_children(self);
// Or skip children entirely
// (don't call visit_children)
}
RustScript allows interrupting the current visitor to perform a scoped traversal on a specific node using a different set of rules. This bridges the impedance mismatch between Babel's path.traverse (graph walk) and SWC's visit_mut_with (recursive function call).
Use traverse(node) { ... } to define a one-off visitor for a subtree:
fn visit_function_declaration(func: &mut FunctionDeclaration, ctx: &Context) {
for stmt in &mut func.body.stmts {
if stmt.is_if_statement() {
// Spawn a nested visitor for just this statement
traverse(stmt) {
// Local state (becomes struct fields in Rust, object properties in JS)
let found_returns = 0;
fn visit_return_statement(ret: &mut ReturnStatement, ctx: &Context) {
ret.argument = None;
self.found_returns += 1;
}
}
}
}
}
Compiles to:
// Babel
const __nestedVisitor = {
state: { found_returns: 0 },
ReturnStatement(path) {
const ret = path.node;
ret.argument = null;
this.state.found_returns++;
},
};
stmt.traverse(__nestedVisitor);// SWC (with hoisted struct)
struct __InlineVisitor_0 {
found_returns: i32,
}
impl VisitMut for __InlineVisitor_0 {
fn visit_mut_return_stmt(&mut self, ret: &mut ReturnStmt) {
ret.arg = None;
self.found_returns += 1;
}
}
// At usage site:
let mut __visitor = __InlineVisitor_0 { found_returns: 0 };
stmt.visit_mut_with(&mut __visitor);Capture Rules:
- Immutable: Ambient variables from the parent scope can be read (cloned into the new visitor)
- Mutable: Ambient variables cannot be mutated from inside
traverse(Rust borrow checker). Pass data via initial state instead.
Use traverse(node) using VisitorName to apply a separately defined plugin/visitor:
plugin CleanUp {
fn visit_identifier(n: &mut Identifier, ctx: &Context) {
// cleanup logic
}
}
plugin Main {
fn visit_function(node: &mut Function, ctx: &Context) {
if node.is_async {
// Route this subtree through the CleanUp visitor
traverse(node) using CleanUp;
}
}
}
Compiles to:
// Babel
node.traverse(CleanUp);// SWC
let mut __visitor = CleanUp::default();
node.visit_mut_with(&mut __visitor);This pattern is required when you want to selectively visit children:
fn visit_block_statement(node: &mut BlockStatement, ctx: &Context) {
// 1. Do NOT call node.visit_children(self);
// 2. Manually iterate
for stmt in &mut node.stmts {
if needs_special_handling(stmt) {
traverse(stmt) using SpecialVisitor;
} else {
// Continue with current visitor
stmt.visit_with(self);
}
}
}
RustScript guarantees that the visitor runs on the node passed to traverse, not just its children:
- SWC: Maps to
node.visit_mut_with(&mut visitor) - Babel: Maps to
path.traverse(visitor)+ manual visit ofpath.node
This ensures consistent behavior across both targets.
Pattern matching that works on both targets:
if matches!(node.callee, MemberExpression {
object: Identifier { name: "console" },
property: Identifier { name: "log" }
}) {
// matched
}
Compiles to:
// Babel
if (
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, { name: "console" }) &&
t.isIdentifier(node.callee.property, { name: "log" })
) {
// matched
}// SWC
if let Callee::Expr(expr) = &node.callee {
if let Expr::Member(member) = &**expr {
if let Expr::Ident(obj) = &*member.obj {
if &*obj.sym == "console" {
if let MemberProp::Ident(prop) = &member.prop {
if &*prop.sym == "log" {
// matched
}
}
}
}
}
}match node.operator {
"+" => handle_add(),
"-" => handle_sub(),
"*" | "/" => handle_mul_div(),
_ => handle_default(),
}
if let Some(name) = get_identifier_name(node) {
// use name
}
Compiles to:
// Babel
const name = getIdentifierName(node);
if (name != null) {
// use name
}// SWC
if let Some(name) = get_identifier_name(node) {
// use name
}| RustScript | Babel (ESTree) | SWC |
|---|---|---|
Identifier |
t.Identifier |
Ident |
CallExpression |
t.CallExpression |
CallExpr |
MemberExpression |
t.MemberExpression |
MemberExpr |
BinaryExpression |
t.BinaryExpression |
BinExpr |
UnaryExpression |
t.UnaryExpression |
UnaryExpr |
StringLiteral |
t.StringLiteral |
Str |
NumericLiteral |
t.NumericLiteral |
Number |
BooleanLiteral |
t.BooleanLiteral |
Bool |
NullLiteral |
t.NullLiteral |
Null |
ArrayExpression |
t.ArrayExpression |
ArrayLit |
ObjectExpression |
t.ObjectExpression |
ObjectLit |
ArrowFunctionExpression |
t.ArrowFunctionExpression |
ArrowExpr |
FunctionDeclaration |
t.FunctionDeclaration |
FnDecl |
VariableDeclaration |
t.VariableDeclaration |
VarDecl |
IfStatement |
t.IfStatement |
IfStmt |
ReturnStatement |
t.ReturnStatement |
ReturnStmt |
BlockStatement |
t.BlockStatement |
BlockStmt |
ExpressionStatement |
t.ExpressionStatement |
ExprStmt |
JSXElement |
t.JSXElement |
JSXElement |
JSXAttribute |
t.JSXAttribute |
JSXAttr |
JSXExpressionContainer |
t.JSXExpressionContainer |
JSXExprContainer |
JSXText |
t.JSXText |
JSXText |
| RustScript | Babel (ESTree) | SWC |
|---|---|---|
TSInterfaceDeclaration |
TSInterfaceDeclaration |
TsInterfaceDecl |
TSPropertySignature |
TSPropertySignature |
TsPropertySignature |
TSMethodSignature |
TSMethodSignature |
TsMethodSignature |
TSTypeReference |
TSTypeReference |
TsTypeRef |
TSTypeAnnotation |
TSTypeAnnotation |
TsTypeAnn |
TSTypeAliasDeclaration |
TSTypeAliasDeclaration |
TsTypeAliasDecl |
Note: TypeScript nodes use the TS prefix in RustScript to distinguish them from JavaScript nodes. When accessing fields on TypeScript nodes, be aware that:
keyonTSPropertySignatureisBox<Expr>in SWC (requires pattern matching to extract identifier name)type_argsonCallExpressionprovides access to generic type arguments likeuseState<string>
// Creating nodes
let id = Identifier::new("myVar");
let call = CallExpression {
callee: id,
arguments: vec![StringLiteral::new("arg")],
};
Compiles to:
// Babel
const id = t.identifier("myVar");
const call = t.callExpression(id, [t.stringLiteral("arg")]);// SWC
let id = Ident::new("myVar".into(), DUMMY_SP);
let call = CallExpr {
callee: Callee::Expr(Box::new(Expr::Ident(id))),
args: vec![ExprOrSpread {
expr: Box::new(Expr::Lit(Lit::Str(Str::from("arg")))),
spread: None,
}],
..Default::default()
};// Type checking
if node.is_identifier() { }
if node.is_call_expression() { }
Compiles to:
// Babel
if (t.isIdentifier(node)) { }
if (t.isCallExpression(node)) { }// SWC
if matches!(node, Expr::Ident(_)) { }
if matches!(node, Expr::Call(_)) { }All strings in RustScript use the Str type:
let name: Str = "hello";
if node.name == "console" {
// works on both targets
}
Compiles to:
// Babel
if (node.name === "console") { }// SWC: JsWord implements PartialEq<&str>
if &*node.sym == "console" { }| RustScript | Babel (JS) | SWC (Rust) |
|---|---|---|
s.starts_with("x") |
s.startsWith("x") |
s.starts_with("x") |
s.ends_with("x") |
s.endsWith("x") |
s.ends_with("x") |
s.contains("x") |
s.includes("x") |
s.contains("x") |
s.len() |
s.length |
s.len() |
s.is_empty() |
s.length === 0 |
s.is_empty() |
s.to_uppercase() |
s.toUpperCase() |
s.to_uppercase() |
s.to_lowercase() |
s.toLowerCase() |
s.to_lowercase() |
let msg = format!("Hello, {}!", name);
Compiles to:
// Babel
const msg = `Hello, ${name}!`;// SWC
let msg = format!("Hello, {}!", name);let some_value = Some(42);
let no_value: Option<i32> = None;
// Safe unwrap with default
let value = opt.unwrap_or(default);
let value = opt.unwrap_or_else(|| compute_default());
// Conditional unwrap
if let Some(v) = opt {
// use v
}
// Map transformation
let mapped = opt.map(|v| v + 1);
// Chain options
let result = opt.and_then(|v| other_option(v));
Compiles to:
// Babel
const value = opt ?? default;
const value = opt ?? computeDefault();
if (opt != null) {
const v = opt;
// use v
}
const mapped = opt != null ? opt + 1 : null;
const result = opt != null ? otherOption(opt) : null;// SWC: Direct Rust Option methods
let value = opt.unwrap_or(default);
let value = opt.unwrap_or_else(|| compute_default());
if let Some(v) = opt {
// use v
}
let mapped = opt.map(|v| v + 1);
let result = opt.and_then(|v| other_option(v));let mut items: Vec<i32> = vec![];
items.push(1);
items.push(2);
let first = items.get(0); // Option<&i32>
let len = items.len();
let is_empty = items.is_empty();
for item in &items {
// iterate
}
let doubled: Vec<i32> = items.iter().map(|x| x * 2).collect();
let mut map: HashMap<Str, i32> = HashMap::new();
map.insert("key", 42);
let value = map.get("key"); // Option<&i32>
let has_key = map.contains_key("key");
for (key, value) in &map {
// iterate
}
let mut set: HashSet<Str> = HashSet::new();
set.insert("item");
let has_item = set.contains("item");
if condition {
// then
} else if other_condition {
// else if
} else {
// else
}
match value {
Pattern1 => expression1,
Pattern2 | Pattern3 => expression2,
_ => default_expression,
}
// For-in loop
for item in collection {
// body
}
// While loop
while condition {
// body
}
// Loop with break
loop {
if done {
break;
}
}
fn process(node: &Node) -> Option<Str> {
if !node.is_valid() {
return None;
}
// continue processing
Some(node.name.clone())
}
// These are always available
Option, Some, None
Vec, vec!
HashMap, HashSet
Str
format!
matches!
/// Check if identifier matches a name
fn is_identifier_named(node: &Identifier, name: &Str) -> bool;
/// Get identifier name safely
fn get_identifier_name(expr: &Expression) -> Option<Str>;
/// Check if node is a specific literal value
fn is_literal_value<T>(node: &Expression, value: T) -> bool;
/// Convert to PascalCase
fn to_pascal_case(s: &Str) -> Str;
/// Convert to camelCase
fn to_camel_case(s: &Str) -> Str;
/// Convert to snake_case
fn to_snake_case(s: &Str) -> Str;
/// Escape string for code generation
fn escape_string(s: &Str) -> Str;
RustScript uses the Result<T, E> type for error handling, which compiles to different representations in Babel and SWC.
fn parse_value(s: &Str) -> Result<i32, Str> {
if s.is_empty() {
return Err("Empty string");
}
Ok(42)
}
Babel Compilation:
function parse_value(s) {
if (s.length === 0) {
return { ok: false, error: "Empty string" };
}
return { ok: true, value: 42 };
}SWC Compilation:
fn parse_value(s: &String) -> Result<i32, String> {
if s.is_empty() {
return Err("Empty string".to_string());
}
Ok(42)
}The ? operator provides automatic error propagation, early-returning on errors.
fn process_file(path: &Str) -> Result<(), Str> {
let ast = parser::parse_file(path)?; // Early return on error
// Use ast...
Ok(())
}
Babel Compilation:
function process_file(path) {
const __result = parser.parse_file(path);
if (!__result.ok) {
return { ok: false, error: __result.error };
}
const ast = __result.value;
// Use ast...
return { ok: true, value: undefined };
}SWC Compilation:
fn process_file(path: &String) -> Result<(), String> {
let ast = parser::parse_file(path)?; // Native Rust ? operator
// Use ast...
Ok(())
}Result Representation:
- Babel (JavaScript):
{ ok: boolean, value?: T, error?: E } - SWC (Rust): Native
Result<T, E>enum
fn validate(input: &Str) -> Result<Str, Str> {
if input.len() > 0 {
Ok(input.clone())
} else {
Err("Input is empty")
}
}
Both Ok() and Err() compile to appropriate Result representations in each target platform.
my-plugin/
src/
lib.rs # Main RustScript source
helpers.rs # Helper functions
dist/
index.js # Babel output
plugin.wasm # SWC WASM output
Cargo.toml # Generated for Rust build
package.json # Generated for npm
rustscript build
# Options
rustscript build --target babel # JS only
rustscript build --target swc # Rust/WASM only
rustscript build --target both # Default: both targets- Parse: RustScript source → AST
- Type Check: Validate types and ownership
- Lower: Resolve macros, statement lowering
- Emit JS: Generate Babel plugin
- Emit Rust: Generate SWC plugin
- Bundle: Package for distribution
/// Plugin that transforms React hooks for analysis
plugin HookAnalyzer {
// State tracked across the visitor
struct State {
hooks: Vec<HookInfo>,
current_component: Option<Str>,
}
struct HookInfo {
name: Str,
hook_type: Str,
component: Str,
}
fn visit_function_declaration(node: &mut FunctionDeclaration, ctx: &Context) {
// Check if this is a component (PascalCase name)
let name = node.id.name.clone();
if is_component_name(&name) {
self.state.current_component = Some(name);
}
// Visit children
node.visit_children(self);
// Clear component context
self.state.current_component = None;
}
fn visit_call_expression(node: &mut CallExpression, ctx: &Context) {
// Check for hook calls
if let Some(component) = &self.state.current_component {
if let Some(name) = get_callee_name(&node.callee) {
if name.starts_with("use") {
self.state.hooks.push(HookInfo {
name: name.clone(),
hook_type: categorize_hook(&name),
component: component.clone(),
});
}
}
}
// Visit children
node.visit_children(self);
}
}
// Helper functions
fn is_component_name(name: &Str) -> bool {
if name.is_empty() {
return false;
}
let first_char = name.chars().next().unwrap();
first_char.is_uppercase()
}
fn get_callee_name(callee: &Expression) -> Option<Str> {
if let Expression::Identifier(id) = callee {
Some(id.name.clone())
} else {
None
}
}
fn categorize_hook(name: &Str) -> Str {
match name.as_str() {
"useState" | "useReducer" => "state".into(),
"useEffect" | "useLayoutEffect" => "effect".into(),
"useRef" => "ref".into(),
"useMemo" | "useCallback" => "memoization".into(),
_ => "custom".into(),
}
}
This section supports "Read-Only" visitors used for transpilation rather than transformation (e.g., TSX to C#).
Writers use lifecycle hooks to manage state during traversal:
pub fn pre()(orfn init()) - Initialize state before traversal beginspub fn exit()(orfn finish()) - Generate output after traversal completespub fn visit_*(...)- Read-only visitor methods
Note: init() and finish() are convenient aliases for pre() and exit() respectively.
writer TsxToCSharp {
// Internal state (define as struct fields)
struct State {
builder: CodeBuilder,
}
// Pre-hook: Initialize state before traversal
pub fn pre() -> State {
State { builder: CodeBuilder::new() }
}
// Read-Only Visitor (Note: `node` is immutable &T, not &mut T)
pub fn visit_jsx_element(node: &JSXElement) {
let tag = node.opening_element.name.get_name();
self.builder.append("new ");
self.builder.append(tag);
self.builder.append("({");
// Recurse manually or use helpers
self.visit_jsx_attributes(&node.opening_element.attributes);
self.builder.append("})");
}
// Exit-hook: Called after all visitors complete
pub fn exit(&self) -> Str {
self.builder.to_string()
}
}
Compiles to:
// Babel: Writer with pre/exit hooks
module.exports = function({ types: t }) {
let state;
return {
pre(file) {
// Call the pre hook to initialize state
state = { builder: new CodeBuilder() };
},
visitor: {
JSXElement(path) {
const node = path.node;
const tag = node.openingElement.name.name;
state.builder.append("new ");
state.builder.append(tag);
state.builder.append("({");
// ... attribute handling ...
state.builder.append("})");
}
},
post(file) {
// Call the exit hook to get output
const output = state.builder.toString();
file.metadata.output = output;
}
}
}// SWC: Uses Visit (read-only) instead of VisitMut
pub struct TsxToCSharp {
builder: CodeBuilder,
}
impl TsxToCSharp {
// Called by the pre hook
pub fn new() -> Self {
Self { builder: CodeBuilder::new() }
}
// Called by the exit hook
pub fn finish(self) -> String {
self.builder.to_string()
}
}
impl Visit for TsxToCSharp {
fn visit_jsx_element(&mut self, n: &JSXElement) {
let tag = get_jsx_element_name(&n.opening.name);
self.builder.append("new ");
self.builder.append(&tag);
self.builder.append("({");
// ... attribute handling ...
self.builder.append("})");
}
}| Aspect | plugin |
writer |
|---|---|---|
| Visitor Type | VisitMut (mutable) |
Visit (read-only) |
| Purpose | Transform AST | Generate output |
| Node Access | &mut T |
&T |
| Output | Modified AST | String/CodeBuilder |
writer ReactToOrleans {
struct State {
builder: CodeBuilder,
}
pub fn pre() -> State {
State { builder: CodeBuilder::new() }
}
pub fn visit_function_declaration(node: &FunctionDeclaration) {
self.builder.append("public class ");
self.builder.append(node.id.name.clone());
self.builder.append(" : Grain, I");
self.builder.append(node.id.name.clone());
self.builder.append(" {\n");
self.builder.indent();
// Visit children (logic to convert hooks to state fields)
node.visit_children(self);
self.builder.dedent();
self.builder.append("}\n");
}
pub fn visit_call_expression(node: &CallExpression) {
// Check for useState hooks
if let Some(name) = get_callee_name(&node.callee) {
if name == "useState" {
self.emit_state_field(node);
return;
}
}
node.visit_children(self);
}
fn emit_state_field(&mut self, node: &CallExpression) {
// Extract state name from parent (const [x, setX] = useState(...))
// This would use ctx or tracked state
self.builder.append("private ");
self.builder.append("dynamic"); // infer type
self.builder.append(" _state;\n");
}
pub fn exit(&self) -> Str {
self.builder.to_string()
}
}
Alternative with init/finish aliases:
writer ReactToOrleans {
struct State {
builder: CodeBuilder,
}
// init() is an alias for pre()
fn init() -> State {
State { builder: CodeBuilder::new() }
}
pub fn visit_function_declaration(node: &FunctionDeclaration) {
self.builder.append("public class ");
self.builder.append(node.id.name.clone());
self.builder.append(" {\n");
self.builder.append("}\n");
}
// finish() is an alias for exit()
fn finish(&self) -> Str {
self.builder.to_string()
}
}
Both styles compile to the same Babel pre() and post() hooks and SWC Visit trait with lifecycle methods.
Because SWC does not track scope by default, accessing ctx.scope triggers a pre-pass analysis in the SWC target.
fn visit_identifier(node: &mut Identifier, ctx: &Context) {
// WARNING: This call is cheap in Babel but expensive in SWC (requires O(n) pre-pass)
if ctx.scope.has_binding(&node.name) {
// ...
}
}
| Method | Description | Babel Cost | SWC Cost |
|---|---|---|---|
has_binding(name) |
Check if name is bound in scope | O(1) | O(n) pre-pass |
generate_uid(hint) |
Generate unique identifier | O(1) | O(n) tracking |
get_binding(name) |
Get binding info | O(1) | O(n) pre-pass |
For performance in SWC, track bindings manually when possible:
plugin OptimizedVisitor {
// Track bindings ourselves
bindings: HashSet<Str>,
fn visit_variable_declarator(node: &mut VariableDeclarator, ctx: &Context) {
if let Pattern::Identifier(id) = &node.id {
self.bindings.insert(id.name.clone());
}
node.visit_children(self);
}
fn visit_identifier(node: &mut Identifier, ctx: &Context) {
// Use our tracked bindings instead of ctx.scope
if self.bindings.contains(&node.name) {
// ...
}
}
}
- Async/await (different semantics between targets)
- External library imports (no cross-platform guarantee)
- Direct DOM/Node.js APIs
- Regex literals (use string-based matching instead)
- Closures that capture mutable state (borrow checker issues)
For cases requiring platform-specific code:
#[cfg(target = "babel")]
fn babel_specific() {
// Only compiled to Babel
}
#[cfg(target = "swc")]
fn swc_specific() {
// Only compiled to SWC
}
Warning: Use sparingly. This breaks the "write once" guarantee.
- Derive macros for common patterns
- LSP support for editor integration
- Source maps for debugging
- Advanced module features (re-exports, wildcards)
- Generic constraints on structs and enums
- Full trait system with trait bounds
- Package registry for shared RustScript libraries
- Testing framework with dual-target test runner
- Benchmark suite comparing Babel vs SWC performance
program = plugin_decl ;
plugin_decl = "plugin" IDENT "{" plugin_body "}" ;
plugin_body = { struct_decl | fn_decl | impl_block } ;
struct_decl = "struct" IDENT "{" struct_fields "}" ;
struct_fields = { IDENT ":" type "," } ;
fn_decl = ["pub"] "fn" IDENT "(" params ")" ["->" type] block ;
params = [ param { "," param } ] ;
param = IDENT ":" type ;
type = primitive_type | reference_type | container_type | tuple_type | IDENT ;
primitive_type = "Str" | "i32" | "f64" | "bool" | "()" ;
reference_type = "&" ["mut"] type ;
container_type = ("Vec" | "Option" | "HashMap" | "HashSet" | "Result") "<" type_args ">" ;
type_args = type { "," type } ;
tuple_type = "(" [ type { "," type } ] ")" ;
block = "{" { statement } "}" ;
statement = let_stmt | expr_stmt | if_stmt | match_stmt | for_stmt
| while_stmt | return_stmt | break_stmt | continue_stmt ;
let_stmt = "let" ["mut"] IDENT [":" type] "=" expr ";" ;
expr_stmt = expr ";" ;
if_stmt = "if" expr block { "else" "if" expr block } [ "else" block ] ;
match_stmt = "match" expr "{" { match_arm } "}" ;
match_arm = pattern "=>" expr "," ;
for_stmt = "for" IDENT "in" expr block ;
while_stmt = "while" expr block ;
return_stmt = "return" [expr] ";" ;
expr = assignment | logical_or ;
assignment = (deref | member) "=" expr | logical_or ;
logical_or = logical_and { "||" logical_and } ;
logical_and = equality { "&&" equality } ;
equality = comparison { ("==" | "!=") comparison } ;
comparison = term { ("<" | ">" | "<=" | ">=") term } ;
term = factor { ("+" | "-") factor } ;
factor = unary { ("*" | "/" | "%") unary } ;
unary = ("!" | "-" | "*" | "&" ["mut"]) unary | call ;
call = primary { "(" args ")" | "." IDENT | "[" expr "]" | "?" } ;
primary = IDENT | literal | "(" expr ")" | struct_init | vec_init | closure | block_expr ;
literal = STRING | NUMBER | "true" | "false" | "null" | "()" ;
struct_init = IDENT "{" { IDENT ":" expr "," } "}" ;
vec_init = "vec!" "[" [ expr { "," expr } ] "]" ;
closure = "|" [ IDENT { "," IDENT } ] "|" ( expr | block ) ;
block_expr = block ;
args = [ expr { "," expr } ] ;
pattern = literal | IDENT | "_" | struct_pattern | tuple_pattern ;
tuple_pattern = "(" [ pattern { "," pattern } ] ")" ;
deref = "*" IDENT ;
member = expr "." IDENT ;RustScript:
fn is_hook_name(name: &Str) -> bool {
name.starts_with("use") && name.len() > 3
}
Babel Output:
function isHookName(name) {
return name.startsWith("use") && name.length > 3;
}SWC Output:
fn is_hook_name(name: &str) -> bool {
name.starts_with("use") && name.len() > 3
}RustScript:
fn get_string_value(node: &Expression) -> Option<Str> {
if let Expression::StringLiteral(lit) = node {
Some(lit.value.clone())
} else {
None
}
}
Babel Output:
function getStringValue(node) {
if (t.isStringLiteral(node)) {
return node.value;
} else {
return null;
}
}SWC Output:
fn get_string_value(node: &Expr) -> Option<String> {
if let Expr::Lit(Lit::Str(lit)) = node {
Some(lit.value.to_string())
} else {
None
}
}error[RS001]: implicit borrow not allowed
--> src/lib.rs:10:15
|
10 | let name = node.name;
| ^^^^^^^^^^ help: use explicit clone: `node.name.clone()`
|
= note: RustScript requires explicit .clone() to extract values from references
error[RS002]: direct property mutation not allowed
--> src/lib.rs:15:5
|
15 | node.name = "new";
| ^^^^^^^^^^^^^^^^^
|
= note: replace the entire node instead of mutating properties
= help: use `*node = NodeType { ... }` pattern
error[RS003]: type mismatch
--> src/lib.rs:20:20
|
20 | let count: Str = 42;
| ^^ expected `Str`, found `i32`
End of Specification