Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
24cb18d
Add CI workflow for cn1playground language smoke tests
liannacasper Apr 7, 2026
da4aed4
Fix playground workflow script invocation permissions
liannacasper Apr 7, 2026
3cd3e09
Fix registry generator classpath for JDK8 tools API
liannacasper Apr 7, 2026
b29fbae
Use Java 17 and portable checks in playground smoke CI
liannacasper Apr 7, 2026
73abb3b
Build dependent modules in playground smoke Maven step
liannacasper Apr 7, 2026
e91778b
Run playground smoke harness from common module classpath
liannacasper Apr 7, 2026
1f9482d
Run playground smoke tests under Xvfb in CI
liannacasper Apr 7, 2026
e1285ac
Install reactor artifacts before running playground harness
liannacasper Apr 7, 2026
792e546
Force smoke harness process exit after success
liannacasper Apr 7, 2026
a6903ea
Add playground syntax matrix harness to smoke test pipeline
liannacasper Apr 7, 2026
e08c2eb
Use Java text blocks in syntax matrix snippets
liannacasper Apr 7, 2026
d2b1cc7
Stabilize syntax matrix outcomes and always exit process
liannacasper Apr 7, 2026
6c21e10
Document playground smoke workflow and syntax rollout process
liannacasper Apr 7, 2026
c7cd472
Expand syntax matrix to table-driven parse/eval coverage
liannacasper Apr 7, 2026
aecea50
Mark try-with-resources matrix cases as current parse gaps
liannacasper Apr 8, 2026
a9281ef
Enable try-with-resources without catch/finally in parser
liannacasper Apr 8, 2026
8af87c8
Throw evaluation error for enhanced-for over null
liannacasper Apr 8, 2026
a72f2af
Use local AutoCloseable resources in TWR matrix cases
liannacasper Apr 8, 2026
c60e28b
Use StringReader resources in TWR matrix cases
liannacasper Apr 8, 2026
a9f1b88
Enable BeanShell class generation for inline class declarations
liannacasper Apr 8, 2026
350450e
Handle superclass invocation checked exceptions in Name
liannacasper Apr 8, 2026
126d6cf
Support inline AutoCloseable class snippets without ASM generator
liannacasper Apr 8, 2026
76c5eca
Restore BeanShell class generation for playground runtime
liannacasper Apr 8, 2026
ea8c221
Handle superclass invocation target exceptions in Name
liannacasper Apr 9, 2026
5f9bbfa
Remove InvocationTargetException from CN1 playground path
liannacasper Apr 9, 2026
1f5abcb
Remove unsupported reflection and regex usage from playground runtime
liannacasper Apr 9, 2026
dae0150
Use CN1 RE/StringUtil for inline AutoCloseable rewrite
liannacasper Apr 9, 2026
04485ff
Rewrite try-resource helper types to AutoCloseable
liannacasper Apr 9, 2026
f087fdf
Stop rewriting inline AutoCloseable helpers in adaptScript
liannacasper Apr 9, 2026
b2c8b94
Enable scripted class declarations in BSHClassDeclaration
liannacasper Apr 9, 2026
7d0b375
Remove reflection/UUID/stream usage from class generation util
liannacasper Apr 10, 2026
890b25b
Restore strict validation hook with faux-reflection checks
liannacasper Apr 10, 2026
ebbbf11
Remove ASM runtime usage and fall back to CN1-safe rewrites
liannacasper Apr 10, 2026
d337468
Add non-bytecode class rewrite path for simple static inner case
liannacasper Apr 10, 2026
729f721
Unify class/anonymous/lambda adaptation pipeline
liannacasper Apr 10, 2026
121a18a
Return Component in inner_class_static_member matrix case
liannacasper Apr 10, 2026
348f40c
Fix static inner class snippet rewrite in playground runner
liannacasper Apr 10, 2026
c2a0d06
Generalize playground support for top-level class declarations
liannacasper Apr 10, 2026
d41cc3e
Add scripted class runtime, expand syntax matrix, fix several languag…
shai-almog Apr 19, 2026
17274ae
Add super.method() dispatch and switch-expression source rewrite
shai-almog Apr 19, 2026
08be026
Rewrite method references to one-arg lambdas before parsing
shai-almog Apr 19, 2026
d0cc6c3
Desugar Java records to a class with final fields and accessors
shai-almog Apr 19, 2026
a7d669d
Add sealed sugar, anon interface impls, and fix record canonical ctor
shai-almog Apr 19, 2026
b2fba66
Expand access registry for collection method dispatch
shai-almog Apr 19, 2026
6b920c5
Prefer scripted classes over imported Java types in declarations
shai-almog Apr 19, 2026
26defa3
Implement common java.util.function SAMs on LambdaValue
shai-almog Apr 19, 2026
5588791
Static nested classes + anon SAM rewrite for known function types
shai-almog Apr 19, 2026
0078f43
Pass enum-constant constructor args through to ScriptedClass.newInstance
shai-almog Apr 19, 2026
b9f5dc2
Support yield form for switch expressions
shai-almog Apr 19, 2026
0d0c371
Per-constant enum method bodies
shai-almog Apr 19, 2026
982d576
Accept LambdaValue at SAM call sites it implements directly
shai-almog Apr 19, 2026
b4202d3
super(args) constructor forwarding + java.util.stream registry classes
shai-almog Apr 19, 2026
02642eb
Overload resolution for scripted classes + integration test broadening
shai-almog Apr 19, 2026
a6c6c8f
Update README + two more integration tests
shai-almog Apr 19, 2026
d8ad61d
Fix multi-level super, nested switch exprs, anon enum inheritance
shai-almog Apr 19, 2026
74618ac
Seven more integration tests + two documented gaps
shai-almog Apr 19, 2026
836ca7b
Ten more integration cases: enum values iteration, lambda composition,
shai-almog Apr 19, 2026
d2575ed
Unblock StringBuilder dispatch in registry
shai-almog Apr 19, 2026
45c11ed
Seven more integration patterns + one small BSH gap documented
shai-almog Apr 19, 2026
ed00cfb
Resolve method-level type parameters as Object in the registry generator
shai-almog Apr 19, 2026
4e3b9f8
Autobox wrapper values for postfix/prefix unary ++/--
shai-almog Apr 19, 2026
7a88a9d
Support nested top-level lambdas + 6 more integration cases
shai-almog Apr 19, 2026
a78f77b
Support var in try-with-resources declarations
shai-almog Apr 20, 2026
758bb88
Eight more integration cases
shai-almog Apr 20, 2026
f0d1898
Eight more diverse integration cases
shai-almog Apr 20, 2026
ef8616d
Accept Character for int-sized numeric params (char-to-int widening)
shai-almog Apr 20, 2026
bc400a9
Seven more integration cases
shai-almog Apr 20, 2026
9fc94f1
Six more integration cases
shai-almog Apr 20, 2026
79b26c7
Inherit default methods from implemented interfaces
shai-almog Apr 20, 2026
b7e702f
Eight more integration patterns
shai-almog Apr 20, 2026
aa9b211
Nine more integration cases
shai-almog Apr 20, 2026
d1c2d8a
Use VARIABLE-typed LHS for scripted-instance field assignments
shai-almog Apr 20, 2026
08405cc
Switch rewriting inside lambda bodies + callstack null-guards
shai-almog Apr 20, 2026
4ab8a38
Eight more CN1-centric integration cases + 2 documented gaps
shai-almog Apr 20, 2026
e6b1296
Treat interface fields as implicitly static
shai-almog Apr 20, 2026
d7eeaf5
Eight more integration cases
shai-almog Apr 20, 2026
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
47 changes: 47 additions & 0 deletions .github/workflows/cn1playground-language.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CN1 Playground Language Tests

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- 'scripts/cn1playground/**'
- '.github/workflows/cn1playground-language.yml'
push:
branches: [main, master]
paths:
- 'scripts/cn1playground/**'
- '.github/workflows/cn1playground-language.yml'
workflow_dispatch:

permissions:
contents: read

jobs:
language-smoke:
name: Playground language smoke
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Java 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven

- name: Ensure Xvfb is available
run: |
set -euo pipefail
if ! command -v xvfb-run >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y xvfb
fi

- name: Run playground language smoke tests
run: |
set -euo pipefail
cd scripts/cn1playground
xvfb-run -a bash tools/run-playground-smoke-tests.sh
154 changes: 94 additions & 60 deletions scripts/cn1playground/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,70 +202,76 @@ The playground uses a customized version of [BeanShell](https://github.com/beans
- **Variable capture**: Lambdas capture variables from enclosing scopes
- **Nested lambdas**: Inner lambdas are rewritten before outer lambdas execute

### Limitations

#### Class Declarations Have Limited Support

BeanShell's class generation is disabled in this playground, but single top-level classes are automatically unwrapped:

```java
// This works - playground unwraps the class:
public class DemoApp {
private Label status; // Becomes script variable: status = null;

public void start() {
status = new Label("Ready");
// ...
}
}
```

**What doesn't work**:
- Nested classes
- Multiple top-level classes
- Interfaces or enums
- Static fields or methods that reference instance fields

For complex state management, consider using loose methods with script-level variables instead of class fields.

#### Instance Field Access in Lambdas

Variables from enclosing scopes work in lambdas, but instance field references without `this` may not resolve correctly:

```java
// Works:
String prefix = "Hello ";
button.addActionListener(e -> status.setText(prefix + "World"));

// May not work as expected if 'status' is an instance field:
// Use 'this.status' or move the field to script scope
```

#### Generic Type Parameters Are Erased

Generic type parameters are erased at runtime. Methods that rely on specific generic types may require casting:
### Class, Interface, Enum, and Record Support

The playground includes a CN1-safe scripted-class runtime so user-declared
types work without runtime bytecode generation or reflection. The
`PlaygroundSyntaxMatrixHarness` (in `common/src/test/java/...`) is a
table-driven matrix that pins exactly what is supported — every entry
either reaches `SUCCESS` or documents a known gap with its diagnostic.

What works:

- **Classes** with fields, constructors (overloaded), methods (overloaded),
generic type parameters (`class Pair<T>`), inheritance with method
overrides, and `super.method()` / `super(args)` dispatch.
- **Static nested classes** (`Outer.Inner`) with `Outer.Inner.staticField` /
`new Outer.Inner()` access.
- **Interfaces** with static methods, default methods, and anonymous
implementations (`new Greeter() { public String greet() { ... } }`).
- **Enums** with simple constants, constants taking constructor args,
per-constant method bodies, and built-in `name()` / `ordinal()` /
`values()` / `valueOf()`.
- **Records** (`record Point(int x, int y) {}`) with auto-generated
accessors and an optional body block.
- **Sealed / non-sealed / permits** modifiers parse cleanly (the permit
list is not enforced at runtime — best-effort syntactic support).

What still doesn't work:

- Non-static inner classes that need an enclosing-instance reference.
- Reflection APIs (`java.lang.reflect.*`, `Class.forName`) — forbidden in
CN1 and out of scope.
- Streams via `Collection.stream()` — CN1's `java.util.Collection` backport
doesn't expose `stream()`.

### Lambdas and Method References

- Lambdas in any context: assignment (`Runnable r = () -> {};`), return
expressions, method-call arguments, and as fields.
- Lambdas implement common SAM types directly: `Runnable`, `Supplier`,
`Consumer`, `BiConsumer`, `Function`, `Predicate`, `Comparator`. Other
CN1-specific listener interfaces are wrapped via `PlaygroundListenerBridge`.
- Method references for static (`System.out::println`), bound-instance
(`prefix::concat`), unbound-instance (`String::length` →
`(s) -> s.length()`), and constructor (`ArrayList::new`) forms.

### Switch and Pattern Matching

- Classic switch statements (int, String, fall-through with explicit break).
- Switch expression arrow form: `String s = switch (x) { case 1 -> "one"; default -> "?"; };`.
- Switch expression yield form: `case 1: yield "one"; default: yield "?";`.
- Arrow-form switch statements (no result value).
- Pattern matching for instanceof: `if (o instanceof String s) { use(s); }`.

### Try-with-resources, Multi-catch, var

- `try (Reader r = ...)` with single, multiple, and trailing-semicolon
resource lists.
- Multi-catch `catch (E1 | E2 e)`.
- Local variable type inference with `var` (BSH already treats `var` as a
loose type).

### Generic Type Parameters Are Erased

Generic type parameters are not enforced at runtime. Methods that rely on
specific generic types may require casting:

```java
// Generic types are erased, so explicit casting may be needed:
List<String> items = (List<String>) someMethod();
```

#### Some Java Syntax Is Unsupported

- **Enhanced for-each with arrays**: BeanShell's for-each works for `Collection` types but not for arrays when using the generated access registry. Use traditional `for (int i = 0; i < arr.length; i++)` loops for arrays, or convert to a List first:
```java
// Doesn't work with arrays:
String[] arr = {"a", "b", "c"};
for (String s : arr) { } // May fail

// Workaround:
for (int i = 0; i < arr.length; i++) { String s = arr[i]; }
// Or convert to List:
for (String s : java.util.Arrays.asList(arr)) { }
```
- **Method references**: `Object::toString` syntax is not supported; use lambdas instead
- **Try-with-resources**: Use try/finally blocks manually

## JavaScript Port Considerations

### `BrowserComponent` and Dialogs
Expand Down Expand Up @@ -347,9 +353,37 @@ mvn clean install

```bash
cd scripts/cn1playground
./scripts/run-tests.sh
bash tools/run-playground-smoke-tests.sh
```

This smoke command currently runs:

1. CN1 access registry generation (`tools/generate-cn1-access-registry.sh`).
2. Registry sanity checks (expected/forbidden class entries).
3. `PlaygroundSmokeHarness` end-to-end behavior checks.
4. `PlaygroundSyntaxMatrixHarness` syntax regression checks.

## Language Feature Rollout Process

Use this process when adding or fixing Java syntax support in Playground:

1. **Add/adjust matrix coverage first**
Update `common/src/test/java/com/codenameone/playground/PlaygroundSyntaxMatrixHarness.java` with a focused snippet for the target syntax.
- For currently unsupported syntax, add as `ExpectedOutcome.FAILURE`.
- When support lands, flip that case to `ExpectedOutcome.SUCCESS`.

2. **Implement parser/runtime change in small steps**
Prefer one syntax feature per PR (e.g. method references only) to keep regressions easy to isolate.

3. **Run smoke + syntax matrix locally**
Run `bash tools/run-playground-smoke-tests.sh` from `scripts/cn1playground`.

4. **Require CI green before merge**
The `CN1 Playground Language Tests` workflow runs the same smoke command under CI (`xvfb-run`) and should pass before merging syntax updates.

5. **Document behavior changes**
Update this README's known issues/limitations when syntax support changes so users know what is now supported.

## Known Issues

1. **Parse errors with complex expressions**: BeanShell's parser may fail on some Java syntax. Simplify complex expressions or break them into multiple statements.
Expand All @@ -360,4 +394,4 @@ cd scripts/cn1playground

## Contributing

See the main Codename One repository for contribution guidelines.
See the main Codename One repository for contribution guidelines.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,31 @@ private Object objectAllocation(
if ( args == null)
throw new EvalError( "Null args in new.", this, callstack );

// Lookup class
// Lookup class. Try a regular variable lookup first so that names
// bound to a ScriptedClass (declared earlier in the same script)
// resolve before we force a Java class lookup.
Object scripted = lookupScriptedClass(nameNode.text, callstack);
if (scripted != null) {
ScriptedClass sc = (ScriptedClass) scripted;
// Anonymous-class style: `new Iface() { method bodies }` —
// synthesise a one-off subclass whose instance methods come from
// the body, then construct it. Works for both interfaces and
// regular classes.
boolean hasBody = jjtGetNumChildren() > 2;
if (hasBody) {
BSHBlock body = (BSHBlock) jjtGetChild(2);
ScriptedClass anonClass = ScriptedClass.build(
sc.getName() + "$anon", callstack.top(), body, sc,
callstack, interpreter);
return anonClass.newInstance(args, callstack, interpreter);
}
if (sc.isInterface()) {
throw new EvalError("Cannot instantiate scripted interface "
+ sc.getName() + " directly; use a class that implements it.",
this, callstack);
}
return sc.newInstance(args, callstack, interpreter);
}
Object obj = nameNode.toObject(
callstack, interpreter, true /*force class*/ );

Expand Down Expand Up @@ -112,6 +136,49 @@ private Object objectAllocation(
return constructObject( type, args, callstack, interpreter );
}

private static Object lookupScriptedClass(String rawName, CallStack callstack) {
if (rawName == null || rawName.length() == 0) return null;
if (callstack == null) return null;
// Strip a generic suffix like "Pair<String>" — type arguments are
// erased at runtime in our scripted-class model.
String simpleName = rawName;
int lt = simpleName.indexOf('<');
if (lt >= 0) simpleName = simpleName.substring(0, lt);
// Dotted names like "Outer.Inner": resolve the head as a
// ScriptedClass and walk through nested classes via the static
// namespace of each enclosing class. We split manually because
// String.split(regex) is not available on CN1's restricted String.
java.util.List<String> partsList = new java.util.ArrayList<String>();
int from = 0;
for (int j = 0; j < simpleName.length(); j++) {
if (simpleName.charAt(j) == '.') {
partsList.add(simpleName.substring(from, j));
from = j + 1;
}
}
partsList.add(simpleName.substring(from));
String[] parts = partsList.toArray(new String[partsList.size()]);
if (parts.length == 0) return null;
ScriptedClass current;
try {
Object v = callstack.top().getVariable(parts[0]);
if (!(v instanceof ScriptedClass)) return null;
current = (ScriptedClass) v;
} catch (UtilEvalError ex) {
return null;
}
for (int i = 1; i < parts.length; i++) {
try {
Object next = current.getStaticNameSpace().getVariable(parts[i]);
if (!(next instanceof ScriptedClass)) return null;
current = (ScriptedClass) next;
} catch (UtilEvalError ex) {
return null;
}
}
return current;
}

Object constructFromEnclosingInstance(Object obj, CallStack callstack,
Interpreter interpreter ) throws EvalError {
throw new EvalError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ public Object eval(CallStack callstack, Interpreter interpreter)
renderTypeNode();
this.type = evalType(callstack, interpreter);

if (!AutoCloseable.class.isAssignableFrom(this.getType()))
// `var` and untyped resources resolve to null; defer the
// AutoCloseable check to runtime after the initializer has run.
if (this.type != null && !AutoCloseable.class.isAssignableFrom(this.type)) {
throw new EvalException("The resource type "+ this.type.getName()
+" does not implement java.lang.AutoCloseable.", this, callstack);
}

this.name = this.getDeclarators()[0].name;

Expand All @@ -48,6 +51,22 @@ public Object eval(CallStack callstack, Interpreter interpreter)
this, callstack);
}

// Verify the bound value is actually AutoCloseable when the type was
// inferred (var) rather than declared.
if (this.type == null && this.varThis != null) {
try {
Object value = this.varThis.getValue();
if (value != null && !(value instanceof AutoCloseable)) {
throw new EvalException("The resource value "
+ value.getClass().getName()
+ " does not implement java.lang.AutoCloseable.",
this, callstack);
}
} catch (UtilEvalError e) {
throw e.toEvalError(this, callstack);
}
}

return Primitive.VOID;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
*/
class BSHBinaryExpression extends SimpleNode implements ParserConstants {
public int kind;
/** For pattern-matching `instanceof Type binding`, the binding identifier
* captured at parse time. When non-null, a positive instanceof test binds
* the lhs value to this name in the current namespace before returning
* true. */
public String instanceofBinding;

BSHBinaryExpression(int id) { super(id); }

Expand Down Expand Up @@ -68,8 +73,15 @@ public Object eval( CallStack callstack, Interpreter interpreter)
lhs = Primitive.unwrap(lhs);

// General case - perform the instanceof based on assignable
return Types.isJavaBaseAssignable( rhs, lhs.getClass() )
? Primitive.TRUE : Primitive.FALSE;
boolean matches = Types.isJavaBaseAssignable(rhs, lhs.getClass());
if (matches && instanceofBinding != null) {
try {
callstack.top().setVariable(instanceofBinding, lhs, false);
} catch (UtilEvalError ex) {
throw ex.toEvalError(this, callstack);
}
}
return matches ? Primitive.TRUE : Primitive.FALSE;
}

/*
Expand Down
Loading
Loading