Skip to content

Commit 7df9d89

Browse files
Fix union/record string formatting (#2189)
* Fix union/record string formatting * Fix tests * Minor fixes
1 parent 09642fd commit 7df9d89

File tree

10 files changed

+175
-121
lines changed

10 files changed

+175
-121
lines changed

.vscode/launch.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
55
"version": "0.2.0",
66
"configurations": [
7+
{
8+
"name": "Attach to Node",
9+
"port": 9229,
10+
"request": "attach",
11+
"skipFiles": [
12+
"<node_internals>/**"
13+
],
14+
"type": "pwa-node"
15+
},
716
{
817
"args": [
918
"${workspaceFolder}/build/tests",

src/Fable.Cli/Main.fs

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,14 @@ type State =
318318
ErroredFiles: Set<string>
319319
TestInfo: TestInfo option }
320320

321+
let filterFiles (exclude: string option) (files: string[]) =
322+
files
323+
|> Array.filter (fun file -> file.EndsWith(".fs") || file.EndsWith(".fsx"))
324+
|> fun filesToCompile ->
325+
match exclude with
326+
| Some exclude -> filesToCompile |> Array.filter (fun x -> not(x.Contains(exclude))) // Use regex?
327+
| None -> filesToCompile
328+
321329
let rec startCompilation (changes: Set<string>) (state: State) = async {
322330
let cracked, parsed, filesToCompile =
323331
match state.ProjectCrackedAndParsed with
@@ -370,13 +378,9 @@ let rec startCompilation (changes: Set<string>) (state: State) = async {
370378

371379
let filesToCompile =
372380
filesToCompile
373-
|> Array.filter (fun file -> file.EndsWith(".fs") || file.EndsWith(".fsx"))
381+
|> filterFiles state.CliArgs.Exclude
374382
|> Array.append (Set.toArray state.ErroredFiles)
375383
|> Array.distinct
376-
|> fun filesToCompile ->
377-
match state.CliArgs.Exclude with
378-
| Some exclude -> filesToCompile |> Array.filter (fun x -> not(x.Contains(exclude))) // Use regex?
379-
| None -> filesToCompile
380384

381385
let logs = getFSharpErrorLogs parsed.Project
382386

@@ -442,10 +446,12 @@ let rec startCompilation (changes: Set<string>) (state: State) = async {
442446

443447
let! changes = Watcher.AwaitChanges [
444448
cracked.ProjectFile
445-
yield! cracked.SourceFiles |> Seq.choose (fun f ->
446-
let path = f.NormalizedFullPath
447-
if Naming.isInFableHiddenDir(path) then None
448-
else Some path)
449+
yield! cracked.SourceFiles
450+
|> Array.choose (fun f ->
451+
let path = f.NormalizedFullPath
452+
if Naming.isInFableHiddenDir(path) then None
453+
else Some path)
454+
|> filterFiles state.CliArgs.Exclude
449455
]
450456
return!
451457
{ state with ProjectCrackedAndParsed = Some(cracked, parsed)

src/Fable.Transforms/Replacements.fs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -407,13 +407,15 @@ let toString com (ctx: Context) r (args: Expr list) =
407407
| Number Int32 -> Helper.LibCall(com, "Util", "int32ToString", String, args)
408408
| Number _ -> Helper.InstanceCall(head, "toString", String, tail)
409409
| DeclaredType(ent, _) ->
410-
let moduleName, methodName =
411-
if ent.IsFSharpUnion then "Types", "unionToString"
412-
elif ent.IsFSharpRecord then "Types", "recordToString"
413-
elif ent.IsValueType then "Types", "objectToString"
414-
else "Types", "objectToString"
415-
Helper.LibCall(com, moduleName, methodName, String, [head], ?loc=r)
416-
| _ -> Helper.GlobalCall("String", String, [head])
410+
let methodName =
411+
if ent.IsFSharpUnion then "unionToString"
412+
elif ent.IsFSharpRecord then "recordToString"
413+
elif ent.FullName = Types.ienumerableGeneric then "seqToString"
414+
else "objectToString"
415+
Helper.LibCall(com, "String", methodName, String, [head], ?loc=r)
416+
| Array _ | List _ ->
417+
Helper.LibCall(com, "String", "seqToString", String, [head], ?loc=r)
418+
| _ -> Helper.LibCall(com, "String", "toString", String, [head], ?loc=r)
417419

418420
let getParseParams (kind: NumberExtKind) =
419421
let isFloatOrDecimal, numberModule, unsigned, bitsize =

src/fable-library/Seq.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,9 @@ function __failIfNone<T>(res: Option<T>) {
7979
}
8080

8181
function makeSeq<T>(f: () => Iterator<T>): Iterable<T> {
82-
const seq = {
82+
return {
8383
[Symbol.iterator]: f,
84-
toString: () => "seq [" + Array.from(seq).join("; ") + "]",
8584
};
86-
return seq;
8785
}
8886

8987
function isArrayOrBufferView<T>(xs: Iterable<T>): xs is T[] {

src/fable-library/String.ts

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import { toString as dateToString } from "./Date.js";
22
import Decimal from "./Decimal.js";
33
import Long, * as _Long from "./Long.js";
44
import { escape } from "./RegExp.js";
5+
import { isIterable, isUnionLike } from "./Util.js";
6+
7+
type Numeric = number | Long | Decimal;
8+
9+
export interface IStringable {
10+
ToString(): string;
11+
}
512

613
const fsFormatRegExp = /(^|[^%])%([0+\- ]*)(\d+)?(?:\.(\d+))?(\w)/;
714
const formatRegExp = /\{(\d+)(,-?\d+)?(?:\:([a-zA-Z])(\d{0,2})|\:(.+?))?\}/g;
815

9-
type Numeric = number | Long | Decimal;
10-
1116
// These are used for formatting and only take longs and decimals into account (no bigint)
1217
function isNumeric(x: any) {
1318
return typeof x === "number" || x instanceof Long || x instanceof Decimal;
@@ -219,6 +224,8 @@ function formatOnce(str2: string, rep: any) {
219224
rep = String(rep);
220225
break;
221226
}
227+
} else {
228+
rep = toString(rep);
222229
}
223230
padLength = parseInt(padLength, 10);
224231
if (!isNaN(padLength)) {
@@ -309,6 +316,8 @@ export function format(str: string, ...args: any[]) {
309316
}
310317
} else if (rep instanceof Date) {
311318
rep = dateToString(rep, pattern || format);
319+
} else {
320+
rep = toString(rep)
312321
}
313322
padLength = parseInt((padLength || " ").substring(1), 10);
314323
if (!isNaN(padLength)) {
@@ -318,6 +327,80 @@ export function format(str: string, ...args: any[]) {
318327
});
319328
}
320329

330+
export function isStringable<T>(x: T | IStringable): x is IStringable {
331+
return x != null && typeof (x as IStringable).ToString === "function";
332+
}
333+
334+
function unionToStringPrivate(self: any): string {
335+
const name = self.cases()[self.tag];
336+
if (self.fields.length === 0) {
337+
return name;
338+
} else {
339+
let fields = "";
340+
let withParens = true;
341+
if (self.fields.length === 1) {
342+
const field = toString(self.fields[0]);
343+
withParens = field.indexOf(" ") >= 0;
344+
fields = field;
345+
}
346+
else {
347+
fields = self.fields.map((x: any) => toString(x)).join(", ");
348+
}
349+
return name + (withParens ? " (" : " ") + fields + (withParens ? ")" : "");
350+
}
351+
}
352+
353+
export function unionToString(self: any) {
354+
return isStringable(self) ? self.ToString() : unionToStringPrivate(self);
355+
}
356+
357+
function recordToStringPrivate(self: any, callStack=0): string {
358+
return callStack > 10
359+
? Object.getPrototypeOf(self).constructor.name
360+
: "{ " + Object.entries(self).map(([k, v]) => k + " = " + toString(v, callStack)).join("\n ") + " }";
361+
}
362+
363+
export function recordToString(self: any): string {
364+
return isStringable(self) ? self.ToString() : recordToStringPrivate(self);
365+
}
366+
367+
export function objectToString(self: any): string {
368+
return isStringable(self) ? self.ToString() : Object.getPrototypeOf(self).constructor.name;
369+
}
370+
371+
export function seqToString<T>(self: Iterable<T>): string {
372+
let count = 0;
373+
let str = "[";
374+
for (let x of self) {
375+
if (count === 0) {
376+
str += toString(x);
377+
} else if (count === 100) {
378+
str += "; ...";
379+
break;
380+
} else {
381+
str += "; " + toString(x);
382+
}
383+
count++;
384+
}
385+
return str + "]";
386+
}
387+
388+
export function toString(x: any, callStack=-1): string {
389+
if (x == null || typeof x !== "object") {
390+
return String(x);
391+
} else if (isStringable(x)) {
392+
return x.ToString();
393+
} else if (isIterable(x)) {
394+
return seqToString(x);
395+
} else if (isUnionLike(x)) {
396+
return unionToStringPrivate(x);
397+
} else {
398+
// TODO: Defaulting to recordToString until we have a way
399+
// to tell records apart from other objects in runtime
400+
return recordToStringPrivate(x, callStack + 1);
401+
}
402+
}
403+
321404
export function endsWith(str: string, search: string) {
322405
const idx = str.lastIndexOf(search);
323406
return idx >= 0 && idx === str.length - search.length;

src/fable-library/Types.ts

Lines changed: 10 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
import { IEquatable, IComparable, combineHashCodes, compare, compareArrays, equalArrays, equals, isComparable, isEquatable, isHashable, sameConstructor, isStringable, numberHash, structuralHash } from "./Util.js";
2-
3-
export function objectToString(self: any) {
4-
if (isStringable(self)) {
5-
return self.ToString();
6-
} else {
7-
return Object.getPrototypeOf(self).constructor.name;
8-
}
9-
}
1+
import { IEquatable, IComparable, combineHashCodes, compare, compareArrays, equalArrays, equals, isComparable, isEquatable, isHashable, sameConstructor, numberHash, structuralHash } from "./Util.js";
102

113
// export class SystemObject implements IEquatable<any> {
124

@@ -66,17 +58,17 @@ export class List<T> implements IEquatable<List<T>>, IComparable<List<T>>, Itera
6658
};
6759
}
6860

69-
public toJSON() {
70-
return Array.from(this);
71-
}
61+
// public toJSON() {
62+
// return Array.from(this);
63+
// }
7264

73-
public toString() {
74-
return this.ToString();
75-
}
65+
// public toString() {
66+
// return this.ToString();
67+
// }
7668

77-
public ToString() {
78-
return "[" + Array.from(this).join("; ") + "]";
79-
}
69+
// public ToString() {
70+
// return "[" + Array.from(this).join("; ") + "]";
71+
// }
8072

8173
public GetHashCode() {
8274
const hashes = Array.from(this).map(structuralHash);
@@ -92,19 +84,6 @@ export class List<T> implements IEquatable<List<T>>, IComparable<List<T>>, Itera
9284
}
9385
}
9486

95-
export function unionToString(self: any) {
96-
const name = self.cases()[self.tag];
97-
if (isStringable(self)) {
98-
return self.ToString();
99-
} else if (self.fields.length === 0) {
100-
return name;
101-
} else if (self.fields.length === 1) {
102-
return name + " " + String(self.fields[0]);
103-
} else {
104-
return name + " (" + self.fields.map((x: any) => String(x)).join(",") + ")";
105-
}
106-
}
107-
10887
export function unionToJson(self: any) {
10988
const name = self.cases()[self.tag];
11089
return self.fields.length === 0 ? name : [name].concat(self.fields);
@@ -186,14 +165,6 @@ export function unionCompareTo(self: any, other: any) {
186165
// }
187166
// }
188167

189-
export function recordToString(self: any) {
190-
if (isStringable(self)) {
191-
return self.ToString();
192-
} else {
193-
return "{" + Object.entries(self).map(([k, v]) => k + " = " + String(v)).join(";\n ") + "}";
194-
}
195-
}
196-
197168
export function recordToJson(self: any, getFieldNames?: (arg: any) => any) {
198169
const o: any = {};
199170
const keys = getFieldNames == null ? Object.keys(self) : getFieldNames(self);

src/fable-library/Util.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ export interface IDisposable {
3333
Dispose(): void;
3434
}
3535

36-
export interface IStringable {
37-
ToString(): void;
38-
}
39-
4036
export interface IComparer<T> {
4137
Compare(x: T, y: T): number;
4238
}
@@ -108,16 +104,12 @@ export function isDisposable<T>(x: T | IDisposable): x is IDisposable {
108104
return x != null && typeof (x as IDisposable).Dispose === "function";
109105
}
110106

111-
export function isStringable<T>(x: T | IStringable): x is IStringable {
112-
return x != null && typeof (x as IStringable).ToString === "function";
113-
}
114-
115107
export function sameConstructor(x: any, y: any) {
116108
return Object.getPrototypeOf(x).constructor === Object.getPrototypeOf(y).constructor;
117109
}
118110

119111
export function isUnionLike(x: any) {
120-
return x != null && x.tag != null && x.fields != null && typeof x.cases === "function";
112+
return x != null && typeof x.tag === "number" && Array.isArray(x.fields) && typeof x.cases === "function";
121113
}
122114

123115
export class Comparer<T> implements IComparer<T> {

src/quicktest/QuickTest.fs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ let testCaseAsync msg f =
5353
printfn "%s" ex.StackTrace
5454
} |> Async.StartImmediate)
5555

56-
let measureTime (f: unit -> unit) = emitJsStatement f """
56+
let measureTime (f: unit -> unit) = emitJsStatement () """
5757
//js
5858
const startTime = process.hrtime();
59-
$0();
59+
f();
6060
const elapsed = process.hrtime(startTime);
6161
console.log("Ms:", elapsed[0] * 1e3 + elapsed[1] / 1e6);
6262
//!js
@@ -66,19 +66,3 @@ let measureTime (f: unit -> unit) = emitJsStatement f """
6666
// to Fable.Tests project. For example:
6767
// testCase "Addition works" <| fun () ->
6868
// 2 + 2 |> equal 4
69-
70-
type MyGetter =
71-
abstract Foo: int
72-
abstract MyNumber: int
73-
74-
measureTime <| (fun () ->
75-
let myGetter = { new MyGetter with
76-
member _.Foo = 1
77-
member this.MyNumber = this.Foo + 1 }
78-
79-
let mutable x = 0
80-
for i = 0 to 1000000000 do
81-
x <- x + myGetter.MyNumber
82-
83-
printfn "x = %i" x
84-
)

0 commit comments

Comments
 (0)