diff --git a/.github/workflows/parley.yml b/.github/workflows/parley.yml index 7d0e35871b8..f353ae9ab3b 100644 --- a/.github/workflows/parley.yml +++ b/.github/workflows/parley.yml @@ -42,6 +42,11 @@ on: - 'CodenameOne/src/com/codename1/call/**' - 'CodenameOne/src/com/codename1/webrtc/**' - 'Ports/JavaSE/src/com/codename1/impl/javase/webrtc/**' + # The iOS side of WebRTC: the bridge and the builder that decides how + # it compiles (it needs -fobjc-arc for the framework's __weak headers). + # The iOS job is the only place a WebRTC app is compiled for a device. + - 'Ports/iOSPort/nativeSources/CN1WebRTC.m' + - 'maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java' - 'CodenameOne/src/com/codename1/io/oidc/**' - 'CodenameOne/src/com/codename1/io/rest/**' # The REST client maps its responses through the mapper registry. diff --git a/CodenameOne/src/com/codename1/impl/FileDialogSupport.java b/CodenameOne/src/com/codename1/impl/FileDialogSupport.java index 2d84134ff5e..f486b380167 100644 --- a/CodenameOne/src/com/codename1/impl/FileDialogSupport.java +++ b/CodenameOne/src/com/codename1/impl/FileDialogSupport.java @@ -108,7 +108,7 @@ public void run() { d.placeButtonCommands(new Command[]{cancel, open}); Command picked = d.showAtPosition(2, 2, 2, 2, true); String[] paths = tree.getSelectedPaths(); - if (picked == open && paths.length > 0) { + if (picked == open && paths.length > 0) { //NOPMD CompareObjectsWithEquals deliver(new FileDialogResult(request, paths, -1, null), callback); } else { deliver(FileDialogResult.cancelled(request), callback); diff --git a/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java b/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java index c9b016e07f0..ed9df7ea4e3 100644 --- a/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java +++ b/maven/build-engine/src/main/java/com/codename1/builders/IPhoneBuilder.java @@ -7673,9 +7673,16 @@ && conditionCovers(governingKey, } + // CN1WebRTC.m compiles under either memory model, but the + // WebRTC framework headers it imports declare __weak + // properties, which clang rejects in a file built with + // ARC off ("cannot create __weak reference in file using + // manual reference counting") -- so every WebRTC app + // failed to compile until it was given -fobjc-arc too. String arcPhaseFixScript = usesCn1Vision || usesCn1Language || usesCn1Inference - ? " arc_sources = ['CN1Vision.m', 'CN1Language.m', 'CN1Inference.m']\n" + || usesWebRtc + ? " arc_sources = ['CN1Vision.m', 'CN1Language.m', 'CN1Inference.m', 'CN1WebRTC.m']\n" + " main_target.source_build_phase.files.each do |bf|\n" + " ref = bf.file_ref\n" + " name = ref && File.basename(ref.path || ref.name || '')\n" diff --git a/maven/dart-runtime/src/main/java/dart/core/DartList.java b/maven/dart-runtime/src/main/java/dart/core/DartList.java index 85ef4d3829b..88eded57517 100644 --- a/maven/dart-runtime/src/main/java/dart/core/DartList.java +++ b/maven/dart-runtime/src/main/java/dart/core/DartList.java @@ -709,7 +709,11 @@ public void sortDefault() { sort((Funcs.Func2) null); } - public DartIterable reversed() { + /// Dart's `List.reversed`. Not named `reversed()`: Java 21 added + /// `List.reversed()` returning a `List`, and Android's platform jar (API 35+) + /// carries it, so a method of that name returning an `Iterable` fails to + /// compile in every generated Android project. + public DartIterable reversedIterable() { DartList self = this; return DartIterable.wrap(() -> new java.util.Iterator() { // Dart's ListIterator: the length at creation, checked on every step, so diff --git a/maven/dart-runtime/src/main/java/dart/core/DateTime.java b/maven/dart-runtime/src/main/java/dart/core/DateTime.java index 3dee4848bb5..4ed1714da99 100644 --- a/maven/dart-runtime/src/main/java/dart/core/DateTime.java +++ b/maven/dart-runtime/src/main/java/dart/core/DateTime.java @@ -66,10 +66,128 @@ public static DateTime utc(long year, long month, long day, long hour, long minu } /// Dart's own grammar for DateTime.parse: an ISO 8601 date, an optional time - /// after `T` or a space, an optional fraction, and an optional `Z` or offset. - private static final java.util.regex.Pattern ISO = java.util.regex.Pattern.compile( - "^([+-]?\\d{4,6})-?(\\d\\d)-?(\\d\\d)(?:[ T](\\d\\d)(?::?(\\d\\d)(?::?(\\d\\d)(?:[.,](\\d+))?)?)?" - + "( ?[zZ]| ?([-+])(\\d\\d)(?::?(\\d\\d))?)?)?$"); + /// after `T` or a space, an optional fraction, and an optional `Z` or offset -- + /// as a regular expression, + /// `^([+-]?\d{4,6})-?(\d\d)-?(\d\d)(?:[ T](\d\d)(?::?(\d\d)(?::?(\d\d)(?:[.,](\d+))?)?)?( ?[zZ]| ?([-+])(\d\d)(?::?(\d\d))?)?)?$`. + /// + /// Matched by hand rather than with a regex engine: `java.util.regex` does not + /// exist on ParparVM, and `com.codename1.util.regex` would add its whole engine + /// to every Flutter app for one date format. Answers the expression's eleven + /// groups (index 0 unused, an unmatched group null), or null for no match. + /// `DateTimeIsoGrammarTest` holds it to the expression above. + static String[] isoGroups(String s) { + // Only the year's length needs backtracking: \d{4,6} is greedy and the + // month may follow it with no separator. Everything after the year is + // deterministic -- every optional part starts with a character nothing + // else could consume at that point. + int start = s.length() > 0 && (s.charAt(0) == '+' || s.charAt(0) == '-') ? 1 : 0; + for (int len = 6; len >= 4; len--) { + String[] g = isoAfterYear(s, start, len); + if (g != null) { + return g; + } + } + return null; + } + + private static String[] isoAfterYear(String s, int start, int yearDigits) { + int n = s.length(); + int i = start + yearDigits; + if (!digits(s, start, yearDigits)) { + return null; + } + String[] g = new String[12]; + g[1] = s.substring(0, i); + if (i < n && s.charAt(i) == '-') { + i++; + } + if (!digits(s, i, 2)) { + return null; + } + g[2] = s.substring(i, i + 2); + i += 2; + if (i < n && s.charAt(i) == '-') { + i++; + } + if (!digits(s, i, 2)) { + return null; + } + g[3] = s.substring(i, i + 2); + i += 2; + if (i == n) { + return g; + } + char sep = s.charAt(i); + if ((sep != ' ' && sep != 'T') || !digits(s, i + 1, 2)) { + return null; + } + g[4] = s.substring(i + 1, i + 3); + i += 3; + int at = colonDigits(s, i); + if (at >= 0) { + g[5] = s.substring(at - 2, at); + i = at; + at = colonDigits(s, i); + if (at >= 0) { + g[6] = s.substring(at - 2, at); + i = at; + if (i < n && (s.charAt(i) == '.' || s.charAt(i) == ',') && digits(s, i + 1, 1)) { + int e = i + 1; + while (e < n && isDigit(s.charAt(e))) { + e++; + } + g[7] = s.substring(i + 1, e); + i = e; + } + } + } + if (i == n) { + return g; + } + int z = i; + if (s.charAt(i) == ' ') { + i++; + } + if (i < n && (s.charAt(i) == 'z' || s.charAt(i) == 'Z')) { + g[8] = s.substring(z, i + 1); + return i + 1 == n ? g : null; + } + if (i >= n || (s.charAt(i) != '+' && s.charAt(i) != '-') || !digits(s, i + 1, 2)) { + return null; + } + g[9] = s.substring(i, i + 1); + g[10] = s.substring(i + 1, i + 3); + int e = colonDigits(s, i + 3); + if (e >= 0) { + g[11] = s.substring(e - 2, e); + } else { + e = i + 3; + } + g[8] = s.substring(z, e); + return e == n ? g : null; + } + + /// `:?(\d\d)` at `i`: the index after it, or -1. + private static int colonDigits(String s, int i) { + int j = i < s.length() && s.charAt(i) == ':' ? i + 1 : i; + return digits(s, j, 2) ? j + 2 : -1; + } + + private static boolean digits(String s, int from, int count) { + if (from + count > s.length()) { + return false; + } + for (int k = from; k < from + count; k++) { + if (!isDigit(s.charAt(k))) { + return false; + } + } + return true; + } + + private static boolean isDigit(char c) { + return c >= '0' && c <= '9'; + } /// `DateTime.parse`: a value with a `Z` or an offset is UTC, anything else local. public static DateTime parse(String formattedString) { @@ -84,19 +202,19 @@ public static DateTime tryParse(String formattedString) { if (formattedString == null) { return null; } - java.util.regex.Matcher m = ISO.matcher(formattedString); - if (!m.matches()) { + String[] g = isoGroups(formattedString); + if (g == null) { return null; } - long year = Long.parseLong(m.group(1).startsWith("+") ? m.group(1).substring(1) : m.group(1)); - long month = Long.parseLong(m.group(2)); - long day = Long.parseLong(m.group(3)); - long hour = num(m.group(4)); - long minute = num(m.group(5)); - long second = num(m.group(6)); + long year = Long.parseLong(g[1].startsWith("+") ? g[1].substring(1) : g[1]); + long month = Long.parseLong(g[2]); + long day = Long.parseLong(g[3]); + long hour = num(g[4]); + long minute = num(g[5]); + long second = num(g[6]); long milli = 0; long micro = 0; - String frac = m.group(7); + String frac = g[7]; if (frac != null) { String six = (frac + "000000").substring(0, 6); long all = Long.parseLong(six); @@ -108,13 +226,13 @@ public static DateTime tryParse(String formattedString) { // A well-formed date outside DateTime's range is still unparseable: tryParse // answers null (and parse a FormatException), never the constructor's error. try { - if (m.group(8) == null) { + if (g[8] == null) { return new DateTime(year, month, day, hour, minute, second, milli, micro); } DateTime u = utc(year, month, day, hour, minute, second, milli, micro); - if (m.group(9) != null) { - long offset = num(m.group(10)) * 60 + num(m.group(11)); - long sign = m.group(9).equals("-") ? -1 : 1; + if (g[9] != null) { + long offset = num(g[10]) * 60 + num(g[11]); + long sign = g[9].equals("-") ? -1 : 1; long at = u.epochMicros - sign * offset * 60000000L; checkMicrosRange(at); return new DateTime(at, true); diff --git a/maven/dart-runtime/src/test/java/dart/core/DartCoreSemanticsTest.java b/maven/dart-runtime/src/test/java/dart/core/DartCoreSemanticsTest.java index eca33db31a3..9ea544d5bda 100644 --- a/maven/dart-runtime/src/test/java/dart/core/DartCoreSemanticsTest.java +++ b/maven/dart-runtime/src/test/java/dart/core/DartCoreSemanticsTest.java @@ -1723,7 +1723,7 @@ public void reversedThrowsWhenTheListChangesLengthMidLoop() { grow.add(1L); grow.add(2L); assertThrows(ConcurrentModificationError.class, () -> { - for (Long v : grow.reversed()) { + for (Long v : grow.reversedIterable()) { grow.add(v); } }); @@ -1733,7 +1733,7 @@ public void reversedThrowsWhenTheListChangesLengthMidLoop() { shrink.add(3L); // Shrinking used to read past the new end instead. assertThrows(ConcurrentModificationError.class, () -> { - for (Long v : shrink.reversed()) { + for (Long v : shrink.reversedIterable()) { shrink.removeLast(); } }); @@ -1741,7 +1741,7 @@ public void reversedThrowsWhenTheListChangesLengthMidLoop() { same.add(1L); same.add(2L); StringBuilder seen = new StringBuilder(); - for (Long v : same.reversed()) { + for (Long v : same.reversedIterable()) { same.set(0, v); seen.append(v); } diff --git a/maven/dart-runtime/src/test/java/dart/core/DateTimeIsoGrammarTest.java b/maven/dart-runtime/src/test/java/dart/core/DateTimeIsoGrammarTest.java new file mode 100644 index 00000000000..2abb9e92d86 --- /dev/null +++ b/maven/dart-runtime/src/test/java/dart/core/DateTimeIsoGrammarTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2012, Codename One and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Codename One designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Codename One through http://www.codenameone.com/ if you + * need additional information or have any questions. + */ +package dart.core; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Random; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + +/// `DateTime.isoGroups` matches Dart's DateTime.parse grammar by hand, because the +/// device has no `java.util.regex`. The JVM running this test does, so the hand +/// matcher is held to the expression itself: same match, same eleven groups. +class DateTimeIsoGrammarTest { + private static final Pattern ISO = Pattern.compile( + "^([+-]?\\d{4,6})-?(\\d\\d)-?(\\d\\d)(?:[ T](\\d\\d)(?::?(\\d\\d)(?::?(\\d\\d)(?:[.,](\\d+))?)?)?" + + "( ?[zZ]| ?([-+])(\\d\\d)(?::?(\\d\\d))?)?)?$"); + + private static final String[] PIECES = { + "0", "1", "2", "9", "12", "2024", "-", "+", ":", " ", "T", "Z", "z", ".", ",", "x", "01", "30" + }; + + private static void check(String s) { + Matcher m = ISO.matcher(s); + String[] got = DateTime.isoGroups(s); + if (!m.matches()) { + assertEquals(null, got == null ? null : "matched", s); + return; + } + String[] want = new String[12]; + for (int g = 1; g <= 11; g++) { + want[g] = m.group(g); + } + assertArrayEquals(want, got, s); + } + + @Test + void knownForms() { + String[] cases = { + "2024-01-15", "20240115", "2024-01-15T10", "2024-01-15T10Z", "2024-01-15 10:30", + "2024-01-15T1030", "2024-01-15T10:30:00", "2024-01-15T103000.5", "2024-01-15T10:30:00,123456789", + "2024-01-15T10:30:00Z", "2024-01-15T10:30:00 z", "2024-01-15T10:30+05:30", "2024-01-15T10:30 -0800", + "2024-01-15T10:30:00-08", "+123456-01-01T00:00:00.5z", "-002024-01-15", "2024012", "202401155", + "2024-1-15", "2024-01-15T", "2024-01-15T10:", "2024-01-15T10:30:00.", "2024-01-15Z", + "2024-01-15T10:30:00Zjunk", "2024-01-15T10+0", "", "x", "+", "1234567-01-01" + }; + for (String s : cases) { + check(s); + } + } + + @Test + void randomStrings() { + Random r = new Random(42); + for (int i = 0; i < 200000; i++) { + StringBuilder sb = new StringBuilder(); + if (r.nextInt(3) == 0) { + sb.append("2024-01-15T10:30"); + } + int parts = r.nextInt(8); + for (int p = 0; p < parts; p++) { + sb.append(PIECES[r.nextInt(PIECES.length)]); + } + check(sb.toString()); + } + } +} diff --git a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java index 5097407e9bd..abc52194ca3 100644 --- a/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java +++ b/maven/dart-transpiler/src/main/java/com/codename1/dart/transpiler/codegen/JavaEmitter.java @@ -5922,7 +5922,7 @@ private Out emitMemberGet(Out target, String name, Node posNode, Ctx ctx) { return new Out(target.code + ".last()", elem); } if (name.equals("reversed")) { - return new Out(target.code + ".reversed()", TypeRef.of("Iterable", elem)); + return new Out(target.code + ".reversedIterable()", TypeRef.of("Iterable", elem)); } } if (tt.is("Map")) { diff --git a/scripts/flutter-bench/baseline/pr/6082.json b/scripts/flutter-bench/baseline/pr/6082.json new file mode 100644 index 00000000000..0ab177a9d5d --- /dev/null +++ b/scripts/flutter-bench/baseline/pr/6082.json @@ -0,0 +1,20 @@ +{ + "pr": 6082, + "rebaseline": { + "javascript": { + "gallery": { + "code_bytes": { + "from": { + "tolerance": { + "value": 0.005 + }, + "value": 13418004 + }, + "reason": "JavaScript Flutter bench has not built since #6055 (java.util.regex absent on ParparVM); master's nightly size predates #6055's Dart runtime. This PR restores the build; the +0.59% is that runtime. DateTime is parsed by hand, no regex engine added.", + "runs": 1, + "value": 13497605 + } + } + } + } +}