Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/diff_test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Differential Tests

on:
pull_request:
push:
branches:
- main
schedule:
# Nightly run at 02:00 UTC for full corpus comparison.
- cron: "0 2 * * *"

jobs:
diff-test:
name: Differential test (st8 vs d8)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
shared-key: "diff-test"

- name: Build st8 (release)
run: cargo build --package st8 --release

- name: Add st8 to PATH
run: echo "${{ github.workspace }}/target/release" >> "$GITHUB_PATH"

- name: Install d8 (V8 shell)
# Try to install d8 from the jsvu tool. If the install fails (e.g. on
# a restricted network), the step is allowed to fail; diff_test.sh
# gracefully falls back to st8-only verification in that case.
continue-on-error: true
run: |
npm install -g jsvu
jsvu --os=linux64 --engines=v8
echo "$HOME/.jsvu/bin" >> "$GITHUB_PATH"
# jsvu installs d8 under the alias "v8"; create a symlink so
# diff_test.sh can find it as "d8".
ln -sf "$HOME/.jsvu/bin/v8" "$HOME/.jsvu/bin/d8" || true

- name: Run differential tests (PR subset — tests/diff/)
run: |
chmod +x scripts/diff_test.sh
scripts/diff_test.sh --corpus tests/diff

diff-test-nightly:
name: Nightly differential test (full corpus)
runs-on: ubuntu-latest
# Only run the nightly full corpus pass on the scheduled trigger.
if: github.event_name == 'schedule'
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build artifacts
uses: Swatinem/rust-cache@v2
with:
shared-key: "diff-test-nightly"

- name: Build st8 (release)
run: cargo build --package st8 --release

- name: Add st8 to PATH
run: echo "${{ github.workspace }}/target/release" >> "$GITHUB_PATH"

- name: Install d8 (V8 shell)
continue-on-error: true
run: |
npm install -g jsvu
jsvu --os=linux64 --engines=v8
echo "$HOME/.jsvu/bin" >> "$GITHUB_PATH"
ln -sf "$HOME/.jsvu/bin/v8" "$HOME/.jsvu/bin/d8" || true

- name: Run full corpus differential tests
run: |
chmod +x scripts/diff_test.sh
# PR subset first
scripts/diff_test.sh --corpus tests/diff
137 changes: 123 additions & 14 deletions crates/st8/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,138 @@
//! `st8` — Stator JavaScript shell.
//!
//! `st8` is the interactive CLI shell for the Stator engine, analogous to
//! V8's `d8`. It will provide a REPL, script execution, and debugging
//! utilities once the interpreter is functional. For now it prints a
//! placeholder message so the workspace compiles end-to-end.
//! `st8` is the command-line shell for the Stator engine, analogous to
//! V8's `d8`. It executes `.js` files, providing a `print()` global so
//! that differential tests can produce stdout output comparable with d8.
//!
//! # Usage
//!
//! ```text
//! st8 <script.js> # Execute a JavaScript file
//! st8 -e "<code>" # Execute a JavaScript snippet
//! ```
//!
//! Exit codes:
//! - `0` — script completed without an uncaught exception
//! - `1` — uncaught exception or engine error

use std::rc::Rc;

use stator_core::bytecode::bytecode_generator::BytecodeGenerator;
use stator_core::error::StatorError;
use stator_core::interpreter::{Interpreter, InterpreterFrame, NativeFn, set_global};
use stator_core::objects::value::JsValue;
use stator_core::parser::parse;

/// Register the host-supplied globals that `st8` makes available to scripts.
///
/// Currently provides:
/// - `print(…)` — converts each argument to a string (ECMAScript ToString)
/// and writes them to stdout separated by spaces, followed by a newline.
/// Matches d8's `print()` built-in.
fn register_globals() {
set_global(
"print".to_string(),
JsValue::NativeFunction(NativeFn(Rc::new(|args: &[JsValue]| {
let parts: Vec<String> = args
.iter()
.map(|v| v.to_js_string().unwrap_or_else(|_| "[object]".to_string()))
.collect();
println!("{}", parts.join(" "));
Ok(JsValue::Undefined)
}))),
);
}

/// Execute `source` as JavaScript, returning `Ok(())` on success or
/// `Err(message)` when an uncaught exception or engine error occurs.
fn run_source(source: &str) -> Result<(), String> {
let program = parse(source).map_err(|e| match e {
StatorError::SyntaxError(msg) => format!("SyntaxError: {msg}"),
other => format!("{other:?}"),
})?;

let bytecode = BytecodeGenerator::compile_program(&program).map_err(|e| match e {
StatorError::SyntaxError(msg) => format!("SyntaxError: {msg}"),
other => format!("{other:?}"),
})?;

let mut frame = InterpreterFrame::new(bytecode, vec![]);
Interpreter::run(&mut frame).map_err(|e| match e {
StatorError::JsException(msg) => msg,
StatorError::TypeError(msg) => format!("TypeError: {msg}"),
StatorError::ReferenceError(msg) => format!("ReferenceError: {msg}"),
StatorError::RangeError(msg) => format!("RangeError: {msg}"),
other => format!("{other:?}"),
})?;

Ok(())
}

fn main() {
println!("st8: Stator JavaScript shell (not yet implemented)");
register_globals();

let args: Vec<String> = std::env::args().collect();

let source = if args.len() >= 3 && args[1] == "-e" {
// Inline snippet: st8 -e "<code>"
args[2].clone()
} else if args.len() >= 2 {
// File execution: st8 <script.js>
let path = &args[1];
match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("st8: cannot read '{}': {}", path, e);
std::process::exit(1);
}
}
} else {
eprintln!("Usage: st8 <script.js>");
eprintln!(" st8 -e \"<code>\"");
std::process::exit(1);
};

if let Err(msg) = run_source(&source) {
eprintln!("{msg}");
std::process::exit(1);
}
}

#[cfg(test)]
mod tests {
use stator_core::parser::scanner::{Scanner, TokenKind};
use super::*;

#[test]
fn test_run_source_arithmetic() {
assert!(run_source("var x = 1 + 2;").is_ok());
}

#[test]
fn test_run_source_syntax_error() {
let result = run_source("var = ;");
assert!(result.is_err());
let msg = result.unwrap_err();
assert!(
msg.contains("SyntaxError"),
"expected SyntaxError, got: {msg}"
);
}

#[test]
fn test_run_source_throw() {
let result = run_source("throw 42;");
assert!(result.is_err());
}

#[test]
fn test_shell_scanner_tokenises_number_literal() {
let mut s = Scanner::new("42");
let tok = s.next_token().unwrap();
assert_eq!(tok.kind, TokenKind::NumericLiteral);
fn test_register_globals_print_is_callable() {
// Verify that a script calling print() does not error.
register_globals();
assert!(run_source("print(1 + 2);").is_ok());
}

#[test]
fn test_shell_scanner_tokenises_identifier() {
let mut s = Scanner::new("foo");
let tok = s.next_token().unwrap();
assert_eq!(tok.kind, TokenKind::Identifier);
fn test_run_source_if_else() {
assert!(run_source("var x = 1; var r = 0; if (x > 0) { r = x; }").is_ok());
}
}
1 change: 1 addition & 0 deletions crates/stator_core/src/builtins/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,7 @@ fn js_value_to_json_inner(
JsValue::Undefined
| JsValue::Symbol(_)
| JsValue::Function(_)
| JsValue::NativeFunction(_)
| JsValue::Generator(_)
| JsValue::Iterator(_)
| JsValue::Error(_) => Ok(None),
Expand Down
Loading
Loading