Skip to content
Open
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
12 changes: 12 additions & 0 deletions modules/ensemble/lib/framework/error_handling.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,18 @@ class CodeError extends EnsembleError {
error =
'Line: $line in YAML and Line: ${exception.line} within the code block. Error Message: $error';
}

@override
String toString() {
final buffer = StringBuffer(error);
if (detailedError != null && detailedError!.trim().isNotEmpty) {
buffer.write('\n\nDetails:\n${detailedError!.trim()}');
}
if (recovery != null && recovery!.trim().isNotEmpty) {
buffer.write('\n\nRecovery:\n${recovery!.trim()}');
}
return buffer.toString();
}
}

class RuntimeError extends EnsembleError {
Expand Down
18 changes: 18 additions & 0 deletions packages/ensemble_ts_interpreter/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
## [1.3.0] - ES6 Runtime Support

* Added practical ES6 support for `let`, `const`, block scoping, const reassignment errors, and read-before-initialization errors.
* Added template literal evaluation, default parameters, rest parameters, and spread in array literals and function calls.
* Added practical `for...of` loops and per-iteration `let`/`const` bindings so closures capture the expected iteration value.
* Added practical optional chaining (`?.`) and nullish coalescing (`??`) support for app logic.
* Added common ES6 syntax support for single-parameter arrow functions, object/array destructuring declarations, object shorthand properties, enhanced object methods, computed property names, and `Symbol` keys.
* Verified practical `Promise.then(...)` callbacks with arrow functions.
* Added practical `async`/`await` support for async functions and async arrows that await `Promise`, Dart `Future`, and plain values.
* Added practical support for destructuring parameters, destructuring assignment, object spread, `Array.from`, `Array.of`, `new Array(length)`, `Promise.all`, and `Promise.allSettled`.
* Added practical tagged template support for app-level formatting helpers such as currency/string tag functions.
* Added final ES6 convenience coverage for `Array.from` collections/array-like objects, `Object.fromEntries`, `Object.hasOwn`, `Object.getOwnPropertyNames`, `Promise.race`, `Promise.any`, `Promise.prototype.finally`, destructuring defaults/rest, and `for...of` destructuring.
* Added practical `Array.prototype.findLast` and `findLastIndex` support.
* Fixed method references such as `fetchData().then(console.log)` by exposing invokable methods as readable callback values.
* Fixed out-of-range array reads to return JavaScript `undefined` instead of Dart `null`.
* Added focused ES6 compatibility tests while preserving the ES5 runtime test gate and existing ES6 conveniences.
* Updated parser dependency to `parsejs_null_safety` 2.1.0 for the required ES6 AST support.

## [1.2.0] - Practical ES5 Runtime Stability

* Added a documented practical ES5 compatibility baseline with focused regression tests.
Expand Down
118 changes: 118 additions & 0 deletions packages/ensemble_ts_interpreter/doc/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Interpreter Architecture & Execution Model

`ensemble_ts_interpreter` is a pure Dart interpreter that executes JavaScript source code inside Flutter applications. It interprets the Abstract Syntax Tree (AST) nodes generated by `parsejs_null_safety` to run calculations, conditional logic, API callback handling, and dynamic UI state binding.

---

## 1. Execution Flow & Compilation Pipeline

The interpreter executes code through a clean pipeline:

```mermaid
graph TD
Code["JavaScript Code (String)"] --> Parser["Parser (parsejs_null_safety)"]
Parser -->|AST| Interpreter["Interpreter (newjs_interpreter.dart)"]
Interpreter -->|Recursive AST Traversal| Output["Resulting Dart Object"]
```

---

## 2. AST Visitor Evaluation Loop (`newjs_interpreter.dart`)

The core execution engine is defined by `JSInterpreter`, which extends `RecursiveVisitor<dynamic>`.
* **Visitor Evaluation**: It evaluates expressions and statements by overriding AST visitor hooks. For example:
* `visitIf(IfStatement)` evaluates the condition, casts it to a boolean, and conditionally executes the `then` or `otherwise` block.
* `visitBinary(BinaryExpression)` evaluates the left and right operands, then executes the corresponding Dart operator (e.g. arithmetic, logical, or comparison).
* **Control Flow Hijacking**: To support statements like `return`, `break`, and `continue`, the interpreter throws control-flow exceptions (`ControlFlowReturnException`, `ControlFlowBreakException`, `ControlFlowContinueException`) which are caught by parent block/loop visitors to redirect execution.

---

## 3. Scope & Scope Lifecycles

Variable resolution operates on two independent scope context models:

```mermaid
graph TD
subgraph ExecutionScope ["Active Scopes"]
VarScope["Dynamic Scope Context (Context Map)"]
LexScope["Lexical Scope Stack (List<_LexicalContext>)"]
end

VarLookup["Name Reference"] --> LexCheck{"Is name in LexScope?"}
LexCheck -->|Yes| LexResolve["Resolve _LexicalBinding (Checks TDZ)"]
LexCheck -->|No| VarResolve["Resolve from Dynamic Context / Globals"]
```

### A. Dynamic Scope (`Context`)
Variables declared via `var` and function declarations are hoisted to the enclosing function or global context. Context variables are stored in simple key-value maps. The interpreter resolves these dynamically during runtime property lookup.

### B. Lexical Scope (`_LexicalContext`)
Block-level variables (`let` and `const`) are managed on a stack of `_LexicalContext` structures:
* **Scope Entry**: Every `visitBlock` pushes a new `_LexicalContext`.
* **Temporal Dead Zone (TDZ)**: Lexical variables are hoisted as uninitialized bindings. Accessing an uninitialized binding throws a `JSException`.
* **Const Protection**: Const bindings cannot be reassigned; doing so throws a runtime exception.
* **Per-Iteration Scopes**: Loops push a new `_LexicalContext` for every iteration. This ensures closures created inside loops capture a snapshot of the iteration's state rather than sharing a single reference.

---

## 4. The Invokable Framework

To bridge Dart/Flutter widgets with JavaScript scripts, the interpreter uses the `Invokable` framework.

```mermaid
classDiagram
class Invokable {
<<interface>>
+getters() Map
+setters() Map
+methods() Map
+getProperty(prop)
+setProperty(prop, val)
+invokeMethod(method, args)
}
class InvokableObject {
}
class StaticArray {
}
class JSMap {
}

Invokable <|-- InvokableObject
Invokable <|-- StaticArray
Invokable <|-- JSMap
```

### Invokable Interface
Any Dart class that mixes in `Invokable` can be passed directly to the interpreter. The interpreter calls:
* `getters()`: To list properties readable by JS.
* `setters()`: To list properties writable by JS.
* `methods()`: To list functions executable by JS.
This makes native Flutter UI controllers, fields, and action behaviors immediately scriptable in JavaScript.

---

## 5. Type Marshaling & Interoperability

The interpreter marshals data between JavaScript and Dart boundaries:

| JavaScript Concept | Dart Type / Object | Boundary Conversion |
| :--- | :--- | :--- |
| **`null`** | `null` | Directly mapped to Dart `null` |
| **`undefined`** | `jsUndefined` (`JSUndefined`) | Out-of-bounds list index reads or empty returns result in `jsUndefined`. |
| **Numbers / Strings** | `num` / `String` | Maps to standard Dart primitives |
| **Array** | `List<dynamic>` | JS arrays are standard Dart lists |
| **Object** | `Map<dynamic, dynamic>` / `Invokable` | Standard JS objects are represented as Dart Maps or `Invokable` objects. |
| **Symbols** | `JSSymbol` | Encapsulates ES6 Symbol keys for maps |

---

## 6. Asynchronous Chaining & Promises

Asynchronous execution uses Dart's native `Future` systems without blocking UI thread rendering.

* **Analysis**: `_containsAwait` checks if a statement/expression contains `await`. If not, it executes synchronously with zero overhead.
* **Execution**: If `await` is present, it runs in `_executeAsyncStatement`, yielding control back to Dart's event loop via async/await futures.
* **Polyfills**:
* `Promise`: Modeled by `JSPromise` wrapping a Dart `Completer`.
* Promise methods (`all`, `allSettled`, `race`, `any`, `finally`) are mapped directly to Dart `Future` helpers like `Future.wait` and `Future.any`.
* `toFuture()`: Facilitates converting `JSPromise` instances to Dart `Future` objects at the Dart-to-JS integration boundary.
40 changes: 30 additions & 10 deletions packages/ensemble_ts_interpreter/doc/known_supported_js.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Known Supported JavaScript

`ensemble_ts_interpreter` targets practical JavaScript ES5 for app logic inside Ensemble and Flutter apps. Some ES6+ conveniences already work, but ES5 is the compatibility baseline.
`ensemble_ts_interpreter` targets practical JavaScript ES5 for app logic inside Ensemble and Flutter apps. A small ES6+ subset is also supported for common developer ergonomics, but ES5 remains the compatibility baseline.

## Supported Baseline

Expand All @@ -24,13 +24,30 @@ The runtime uses Dart `Map`, `List`, and `Invokable` values at the public bounda
- Sparse array holes are preserved for `delete`, skipped by callback methods, and serialized as `null` by `JSON.stringify`.
- `Function.prototype.call`, `apply`, and `bind` work for interpreter functions and compatible Dart callbacks.

## ES6+ Conveniences That May Work

These features are available today where tests cover them, but they are not the baseline compatibility contract yet:

- Arrow functions.
## Supported ES6 Conveniences

These features are covered by focused package tests:

- Arrow functions, including the common single-parameter form without parentheses.
- `let` and `const` declarations with practical block scoping.
- `const` reassignment errors.
- Read-before-initialization errors for lexical declarations.
- Per-iteration `let` bindings for `for` loops.
- Practical `for...of` loops over arrays/lists, strings, `Map`, and `Set`.
- Destructuring in practical `for...of` declarations and assignment targets.
- Template literals with `${...}` interpolation and practical tagged-template calls.
- Object and array destructuring declarations, defaults, object rest properties, destructuring parameters, and practical destructuring assignment.
- Object property shorthand, enhanced object methods, and computed property names.
- Default parameters and rest parameters for normal functions and arrow functions.
- Spread in array literals, function calls, and object literals.
- `Array.from`, including arrays/lists, strings, `Map`, `Set`, and array-like objects, plus `Array.of` and `new Array(length)`.
- `Object.fromEntries`, `Object.hasOwn`, and `Object.getOwnPropertyNames`.
- Optional chaining with `?.`, including property access, index access, and optional calls.
- Nullish coalescing with `??`, falling back only for practical null/undefined values.
- `Map` and `Set` globals.
- `Promise`, `queueMicrotask`, and promise-style helpers.
- `Symbol` values as computed object keys.
- `Promise`, `Promise.all`, `Promise.allSettled`, `Promise.race`, `Promise.any`, `Promise.prototype.finally`, `queueMicrotask`, and promise-style helpers.
- Practical `async` functions and async arrows with `await` for `Promise`, Dart `Future`, and plain values.
- Some modern array and string helpers such as `includes`, `flat`, `flatMap`, `startsWith`, and `endsWith`.

Treat these as supported conveniences, not full ES6 conformance.
Expand All @@ -39,8 +56,11 @@ Treat these as supported conveniences, not full ES6 conformance.

Do not rely on these areas for production app logic yet:

- `let`, `const`, block scoping, temporal dead zone behavior, and strict mode conformance.
- ES6 classes, modules, imports, exports, destructuring, spread/rest, template literals, generators, and async/await.
- Strict mode conformance.
- ES6 classes, modules, imports, exports, and generators.
- Full tagged-template edge cases such as `strings.raw` identity/caching semantics.
- Full destructuring edge-case coverage beyond practical declaration, parameter, assignment, and `for...of` patterns.
- Full async/await coverage in every statement/expression position; supported app patterns include awaited variable initializers, awaited returns, and simple expressions around awaited values.
- Direct `eval` scope behavior.
- Full Test262 conformance, especially edge cases around property descriptors, coercion, `Date`, `RegExp`, strict mode, and host objects.
- Browser-specific APIs beyond the globals provided by Ensemble.
Expand All @@ -49,7 +69,7 @@ Do not rely on these areas for production app logic yet:

For the most reliable app code:

- Prefer ES5-style functions and `var`.
- Prefer ES5-style functions and `var` for maximum portability; use the documented ES6 subset where tests cover your app pattern.
- Use `Object.*`, array callbacks, and JSON APIs normally, but keep logic app-focused rather than engine-conformance-heavy.
- Treat Dart `null` at the public boundary as the practical representation for JavaScript `null`/`undefined`.
- Add focused interpreter tests when introducing new JavaScript patterns that app screens depend on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ mixin Invokable {
if (func != null) {
return func();
}
final method = getMethod(prop);
if (method != null) {
return (dynamic args) => method(args);
}
throw InvalidPropertyException(
'Object with id:${id ?? ''} does not have a gettable property named $prop');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,22 @@ class JSMapConstructor extends Object with Invokable {

@override
Map<String, Function> methods() => {
'init': () => JSMap({}),
'init': ([List? entries]) {
final map = JSMap({});
if (entries != null) {
for (final entry in entries) {
if (entry is List && entry.length >= 2) {
map._data[entry[0]] = entry[1];
} else if (entry is Map) {
final key = entry.containsKey('key') ? entry['key'] : entry[0];
final value =
entry.containsKey('value') ? entry['value'] : entry[1];
map._data[key] = value;
}
}
}
return map;
},
};

@override
Expand Down Expand Up @@ -39,6 +54,17 @@ class JSMap extends Object with Invokable {

@override
Map<String, Function> setters() => {};

String toConsoleString() {
final entries = _data.entries
.map((entry) =>
'${_formatCollectionValue(entry.key)} => ${_formatCollectionValue(entry.value)}')
.join(', ');
return 'Map(${_data.length}) {$entries}';
}

@override
String toString() => toConsoleString();
}

class JSSetConstructor extends Object with Invokable {
Expand Down Expand Up @@ -72,11 +98,28 @@ class JSSet extends Object with Invokable {
'has': (dynamic value) => _data.contains(value),
'delete': (dynamic value) => _data.remove(value),
'clear': () => _data.clear(),
'keys': () => _data.toList(),
'values': () => _data.toList(),
'entries': () => _data.map((e) => [e, e]).toList(),
'forEach': (Function f) => _data.forEach((e) => f([e, e, this])),
};

@override
Map<String, Function> setters() => {};

String toConsoleString() {
final values = _data.map(_formatCollectionValue).join(', ');
return 'Set(${_data.length}) {$values}';
}

@override
String toString() => toConsoleString();
}

String _formatCollectionValue(dynamic value) {
if (value == null) return 'null';
if (value is String) return '"$value"';
if (value is JSMap) return value.toConsoleString();
if (value is JSSet) return value.toConsoleString();
return value.toString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,29 @@ class InvokableObject extends Object with Invokable {
.map((key) => [key, InvokableController.getProperty(value, key)])
.toList()
: (value == null ? [] : null),
'fromEntries': (dynamic entries) {
final result = <dynamic, dynamic>{};
List<dynamic> entryList = [];
if (entries is List) {
entryList = entries;
} else if (entries is Invokable) {
final method = entries.methods()['entries'];
final value =
method == null ? null : Function.apply(method, const []);
if (value is List) entryList = value;
}
for (final entry in entryList) {
if (entry is List && entry.length >= 2) {
result[entry[0]] = entry[1];
} else if (entry is Map) {
final key = entry.containsKey('key') ? entry['key'] : entry[0];
final value =
entry.containsKey('value') ? entry['value'] : entry[1];
result[key] = value;
}
}
return result;
},
'create': (dynamic proto) {
final Map<String, dynamic> obj = {};
if (proto != null) {
Expand All @@ -179,8 +202,13 @@ class InvokableObject extends Object with Invokable {
},
'getPrototypeOf': (dynamic value) =>
InvokableController.getPrototype(value),
'hasOwn': (dynamic value, dynamic key) =>
InvokableController.hasOwnProperty(value, key),
'hasOwnProperty': (dynamic value, String key) =>
InvokableController.hasOwnProperty(value, key),
'getOwnPropertyNames': (dynamic value) => (value is Map)
? InvokableController.ownPropertyKeys(value)
: (value == null ? [] : null),
'getPropertyNames': (dynamic value) => (value is Map)
? InvokableController.ownPropertyKeys(value)
: (value == null ? [] : null),
Expand Down
Loading
Loading