Skip to content

Commit 9217213

Browse files
authored
[Beam] Fix codegen issues and enable 9 more tests (#4359)
1 parent 962df72 commit 9217213

10 files changed

Lines changed: 146 additions & 106 deletions

File tree

src/Fable.Transforms/Beam/Fable2Beam.fs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ let private isClassType (com: IBeamCompiler) (entityRef: EntityRef) =
5858
&& not entity.IsFSharpModule
5959
&& not entity.IsInterface
6060
&& not entity.IsFSharpExceptionDeclaration
61+
&& not entity.IsValueType
6162
| None -> false
6263
| _ -> false
6364

@@ -332,9 +333,24 @@ let rec transformExpr (com: IBeamCompiler) (ctx: Context) (expr: Expr) : Beam.Er
332333
]
333334

334335
| Delegate(args, body, _name, _tags) ->
336+
// Deduplicate Erlang variable names in arg patterns.
337+
// After uncurrying, inner lambda params that shadow outer ones appear at
338+
// the same level. In Erlang, fun(X, X) -> requires both args to be equal,
339+
// so we replace earlier duplicates with _ (anonymous/unused pattern).
340+
let argNames =
341+
args |> List.map (fun a -> capitalizeFirst a.Name |> sanitizeErlangVar)
342+
335343
let argPats =
336-
args
337-
|> List.map (fun a -> Beam.PVar(capitalizeFirst a.Name |> sanitizeErlangVar))
344+
let lastIndex = System.Collections.Generic.Dictionary<string, int>()
345+
argNames |> List.iteri (fun i name -> lastIndex.[name] <- i)
346+
347+
argNames
348+
|> List.mapi (fun i name ->
349+
if lastIndex.[name] = i then
350+
Beam.PVar(name)
351+
else
352+
Beam.PVar("_")
353+
)
338354

339355
let ctx' =
340356
{ ctx with LocalVars = args |> List.fold (fun s a -> s.Add(a.Name)) ctx.LocalVars }
@@ -2228,6 +2244,41 @@ and transformClassDeclaration
22282244
// F# exception construction goes through NewRecord, not ClassDecl constructor.
22292245
// No need to generate a constructor function here.
22302246
[]
2247+
elif ent.IsValueType then
2248+
// Struct/value type: return a plain map (not a ref) using sanitizeFieldName keys
2249+
// to match the record-style FieldGet path.
2250+
let ctorCtx =
2251+
{ ctx with
2252+
LocalVars = ctorArgs |> List.fold (fun (s: Set<string>) a -> s.Add(a.Name)) ctx.LocalVars
2253+
}
2254+
2255+
let mapEntries =
2256+
fields
2257+
|> List.map (fun (name, value) ->
2258+
let erlValue = transformExpr com ctorCtx value
2259+
atomLit (sanitizeFieldName name), erlValue
2260+
)
2261+
2262+
let body = [ Beam.ErlExpr.Map mapEntries ]
2263+
2264+
let ctorFuncName = sanitizeErlangName cons.Name
2265+
com.RegisterConstructorName ent.FullName ctorFuncName
2266+
2267+
let funcDef: Beam.ErlFunctionDef =
2268+
{
2269+
Name = Beam.Atom ctorFuncName
2270+
Arity = argPatterns.Length
2271+
Clauses =
2272+
[
2273+
{
2274+
Patterns = argPatterns
2275+
Guard = []
2276+
Body = body
2277+
}
2278+
]
2279+
}
2280+
2281+
[ Beam.ErlForm.Function funcDef ]
22312282
else
22322283
// Regular class: object = make_ref(), state in process dict
22332284
let ctorCtx =

src/Fable.Transforms/Beam/Replacements.fs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ let private operators
250250
| Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", _t, [ arg ], ?loc = r) |> Some
251251
| Float16
252252
| Float32
253-
| Float64 -> emitExpr r _t [ arg ] "float_to_binary($0)" |> Some
253+
| Float64 -> Helper.LibCall(com, "fable_convert", "to_string", _t, [ arg ], ?loc = r) |> Some
254254
| _ -> emitExpr r _t [ arg ] "integer_to_binary($0)" |> Some
255255
| Type.Boolean -> emitExpr r _t [ arg ] "atom_to_binary($0)" |> Some
256256
| _ -> Helper.LibCall(com, "fable_convert", "to_string", _t, [ arg ], ?loc = r) |> Some
@@ -323,19 +323,8 @@ let private operators
323323
| "op_PipeLeft2", [ f; x; y ] -> CurriedApply(f, [ x; y ], _t, r) |> Some
324324
| "op_PipeRight3", [ x; y; z; f ]
325325
| "op_PipeLeft3", [ f; x; y; z ] -> CurriedApply(f, [ x; y; z ], _t, r) |> Some
326-
| "op_ComposeRight", [ f1; f2 ] ->
327-
// fun x -> f2(f1(x))
328-
let ident = makeTypedIdent _t "x"
329-
let identExpr = IdentExpr ident
330-
let innerCall = CurriedApply(f1, [ identExpr ], _t, None)
331-
let outerCall = CurriedApply(f2, [ innerCall ], _t, None)
332-
Lambda(ident, outerCall, None) |> Some
333-
| "op_ComposeLeft", [ f2; f1 ] ->
334-
let ident = makeTypedIdent _t "x"
335-
let identExpr = IdentExpr ident
336-
let innerCall = CurriedApply(f1, [ identExpr ], _t, None)
337-
let outerCall = CurriedApply(f2, [ innerCall ], _t, None)
338-
Lambda(ident, outerCall, None) |> Some
326+
| "op_ComposeRight", [ f1; f2 ] -> compose com ctx r _t f1 f2 |> Some
327+
| "op_ComposeLeft", [ f2; f1 ] -> compose com ctx r _t f1 f2 |> Some
339328
// Not (boolean negation)
340329
| "Not", [ operand ] -> makeUnOp r _t operand UnaryNot |> Some
341330
// Tuples
@@ -468,7 +457,7 @@ let private languagePrimitives
468457
let cmp = compare com r left right
469458
makeBinOp r Boolean cmp (makeIntConst 0) BinaryGreaterOrEqual |> Some
470459
| ("PhysicalEquality" | "PhysicalEqualityIntrinsic"), [ left; right ] ->
471-
makeBinOp r Boolean left right BinaryEqual |> Some
460+
emitExpr r Boolean [ left; right ] "$0 =:= $1" |> Some
472461
| ("GenericHash" | "GenericHashIntrinsic"), [ arg ] ->
473462
Helper.LibCall(com, "fable_comparison", "hash", t, [ arg ], ?loc = r) |> Some
474463
| ("PhysicalHash" | "PhysicalHashIntrinsic"), [ arg ] ->
@@ -590,7 +579,9 @@ let private objects
590579
|> Some
591580
| Float16
592581
| Float32
593-
| Float64 -> emitExpr r t [ thisObj ] "float_to_binary($0)" |> Some
582+
| Float64 ->
583+
Helper.LibCall(com, "fable_convert", "to_string", t, [ thisObj ], ?loc = r)
584+
|> Some
594585
| _ -> emitExpr r t [ thisObj ] "integer_to_binary($0)" |> Some
595586
| Type.Boolean -> emitExpr r t [ thisObj ] "atom_to_binary($0)" |> Some
596587
| Type.String -> Some thisObj
@@ -1209,7 +1200,7 @@ let private conversions
12091200
| Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ arg ], ?loc = r) |> Some
12101201
| Float16
12111202
| Float32
1212-
| Float64 -> emitExpr r t [ arg ] "float_to_binary($0)" |> Some
1203+
| Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some
12131204
| _ -> emitExpr r t [ arg ] "integer_to_binary($0)" |> Some
12141205
| Type.Boolean -> emitExpr r t [ arg ] "atom_to_binary($0)" |> Some
12151206
| _ -> Helper.LibCall(com, "fable_convert", "to_string", t, [ arg ], ?loc = r) |> Some
@@ -1249,7 +1240,7 @@ let private numericTypes
12491240
| Decimal -> Helper.LibCall(com, "fable_decimal", "to_string", t, [ c ], ?loc = r) |> Some
12501241
| Float16
12511242
| Float32
1252-
| Float64 -> emitExpr r t [ c ] "float_to_binary($0)" |> Some
1243+
| Float64 -> Helper.LibCall(com, "fable_convert", "to_string", t, [ c ], ?loc = r) |> Some
12531244
| _ -> emitExpr r t [ c ] "integer_to_binary($0)" |> Some
12541245
| _ -> None
12551246
| "ToString", Some c, [ fmt ] ->

src/fable-library-beam/fable_string.erl

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -393,12 +393,10 @@ split_count_loop(Str, Seps, Count, Acc) ->
393393
to_string(V) when is_binary(V) -> V;
394394
to_string(V) when is_integer(V) -> integer_to_binary(V);
395395
to_string(V) when is_float(V) ->
396-
%% Match Erlang float_to_binary default but trim trailing zeros
397-
S = float_to_binary(V, [{decimals, 10}, compact]),
398-
%% Ensure at least one decimal: "42" -> "42.0"
399-
case binary:match(S, <<".">>) of
400-
nomatch -> <<S/binary, ".0">>;
401-
_ -> S
396+
%% Match .NET ToString() "G" format: whole-number floats produce "2" not "2.0"
397+
case V == trunc(V) of
398+
true -> integer_to_binary(trunc(V));
399+
false -> float_to_binary(V, [{decimals, 10}, compact])
402400
end;
403401
to_string(V) when is_atom(V) -> atom_to_binary(V, utf8);
404402
to_string(V) when is_boolean(V) ->
@@ -735,7 +733,12 @@ format_raw(Type, Prec, Value) when Type =:= $g; Type =:= $G ->
735733
trim_trailing_zeros(S)
736734
end;
737735
format_raw($x, _Prec, Value) ->
738-
iolist_to_binary(io_lib:format("~.16b", [trunc(Value)]));
736+
V = trunc(Value),
737+
Masked = if V < 0, abs(V) > 16#FFFFFFFF -> V band 16#FFFFFFFFFFFFFFFF;
738+
V < 0 -> V band 16#FFFFFFFF;
739+
true -> V
740+
end,
741+
iolist_to_binary(io_lib:format("~.16b", [Masked]));
739742
format_raw($X, _Prec, Value) ->
740743
S = format_raw($x, _Prec, Value),
741744
string:uppercase(S);

tests/Beam/ApplicativeTests.fs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1351,13 +1351,11 @@ let ``test Partial active patterns can return bool`` () =
13511351

13521352
// --- Arity record tests ---
13531353

1354-
// TODO: Uncurrying naming collision — generates fun(X, X) -> which in Erlang
1355-
// means both args must be equal (pattern matching), not two separate parameters.
1356-
// [<Fact>]
1357-
// let ``test Arity is checked also when constructing records`` () =
1358-
// let f i j = (i * 2) + (j * 3)
1359-
// let r = { arity2 = fun x -> f x >> fun y -> sprintf "foo%i" y }
1360-
// r.arity2 4 5 |> equal "foo23"
1354+
[<Fact>]
1355+
let ``test Arity is checked also when constructing records`` () =
1356+
let f i j = (i * 2) + (j * 3)
1357+
let r = { arity2 = fun x -> f x >> fun y -> sprintf "foo%i" y }
1358+
r.arity2 4 5 |> equal "foo23"
13611359

13621360
// --- Module/Class values returning lambdas ---
13631361

tests/Beam/ComparisonTests.fs

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -677,16 +677,14 @@ let ``test isNull with objects works`` () =
677677
let s2: String = "hello"
678678
isNull s2 |> equal false
679679

680-
// TODO: PhysicalEquality on ResizeArray (ref) uses fable_comparison:compare instead of =:=
681-
// because BinaryEqual on array/ref types gets structural comparison
682-
// [<Fact>]
683-
// let ``test PhysicalEquality works`` () =
684-
// let r1 = ResizeArray([1; 2])
685-
// let r2 = ResizeArray([1; 2])
686-
// let r3 = r1
687-
// LanguagePrimitives.PhysicalEquality r1 r2 |> equal false
688-
// LanguagePrimitives.PhysicalEquality r2 r2 |> equal true
689-
// LanguagePrimitives.PhysicalEquality r3 r1 |> equal true
680+
[<Fact>]
681+
let ``test PhysicalEquality works`` () =
682+
let r1 = ResizeArray([1; 2])
683+
let r2 = ResizeArray([1; 2])
684+
let r3 = r1
685+
LanguagePrimitives.PhysicalEquality r1 r2 |> equal false
686+
LanguagePrimitives.PhysicalEquality r2 r2 |> equal true
687+
LanguagePrimitives.PhysicalEquality r3 r1 |> equal true
690688

691689
// --- Raw Erlang reference comparison tests ---
692690
// These test that fable_comparison correctly handles Erlang references

tests/Beam/DictionaryTests.fs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,12 @@ let ``test Dictionary KeyValuePattern works`` () =
214214
total <- y + total
215215
equal 385. total
216216

217-
// TODO: IDictionary created via dict — indexer access goes through IDictionary which expects dict ref
218-
// [<Fact>]
219-
// let ``test Interface IDictionary creation works`` () =
220-
// let dic =
221-
// dict
222-
// <| seq { for i in 1. .. 10. -> i.ToString(), i * i }
223-
// equal 4. dic.["2"]
217+
[<Fact>]
218+
let ``test Interface IDictionary creation works`` () =
219+
let dic =
220+
dict
221+
<| seq { for i in 1. .. 10. -> i.ToString(), i * i }
222+
equal 4. dic.["2"]
224223

225224
[<Fact>]
226225
let ``test Dictionary creation from IDictionary works`` () =

tests/Beam/SeqExpressionTests.fs

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,14 @@ let ``test for in seq expressions works`` () =
4646
seq { for x in 1 .. 10 do yield x }
4747
|> Seq.length |> equal 10
4848

49-
// TODO: while in seq generates (fable_seq:delay(...))(ok) double-evaluation — badfun error
50-
// [<Fact>]
51-
// let ``test while in seq expressions works`` () =
52-
// let mutable n = 0
53-
// seq {
54-
// while n < 10 do
55-
// n <- n + 1
56-
// yield n
57-
// } |> Seq.sum |> equal 55
49+
[<Fact>]
50+
let ``test while in seq expressions works`` () =
51+
let mutable n = 0
52+
seq {
53+
while n < 10 do
54+
n <- n + 1
55+
yield n
56+
} |> Seq.sum |> equal 55
5857

5958
[<Fact>]
6059
let ``test recursive seq expressions work`` () =
@@ -69,18 +68,17 @@ let ``test recursive seq expressions work`` () =
6968
let t = Node(Node(Leaf, 1, Leaf), 2, Node(Leaf, 3, Leaf))
7069
traverse t |> Seq.sum |> equal 9
7170

72-
// TODO: try/finally in seq generates (fable_seq:delay(...))(ok) double-evaluation + util:exception not available
73-
// [<Fact>]
74-
// let ``test try finally in seq expressions works`` () =
75-
// let mutable n = 0
76-
// try seq {
77-
// try
78-
// raise (exn "My message")
79-
// finally
80-
// n <- n + 1
81-
// } |> Seq.iter ignore
82-
// with _ -> ()
83-
// equal 1 n
71+
[<Fact>]
72+
let ``test try finally in seq expressions works`` () =
73+
let mutable n = 0
74+
try seq {
75+
try
76+
raise (exn "My message")
77+
finally
78+
n <- n + 1
79+
} |> Seq.iter ignore
80+
with _ -> ()
81+
equal 1 n
8482

8583
[<Fact>]
8684
let ``test array expressions work`` () =

tests/Beam/StringTests.fs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -517,9 +517,9 @@ let ``test sprintf "%X" works`` () =
517517
sprintf "255: %x" 255 |> equal "255: ff"
518518
sprintf "4095L: %X" 4095L |> equal "4095L: FFF"
519519

520-
// TODO: Negative hex formatting requires two's complement masking which isn't implemented
521-
// sprintf "-255: %X" -255 |> equal "-255: FFFFFF01"
522-
// sprintf "-4095L: %X" -4095L |> equal "-4095L: FFFFFFFFFFFFF001"
520+
sprintf "-255: %X" -255 |> equal "-255: FFFFFF01"
521+
// TODO: int64 negative hex needs 64-bit mask but Erlang can't distinguish int32 from int64 at runtime
522+
// sprintf "-4095L: %X" -4095L |> equal "-4095L: FFFFFFFFFFFFF001"
523523

524524
[<Fact>]
525525
let ``test sprintf integers with sign and padding works`` () =

tests/Beam/TailCallTests.fs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,17 @@ module Functions =
5757
| 0 -> x
5858
| _ -> iterate f (n - 1) (f x)
5959

60+
let recWithFinally () =
61+
let mutable log = ""
62+
let rec test n =
63+
try
64+
log <- log + string "abcde".[n]
65+
if n < 4 then test (n+1)
66+
finally
67+
log <- log + string "ABCDE".[n]
68+
test 0
69+
log
70+
6071
open Functions
6172

6273
module Issue3301 =
@@ -195,12 +206,6 @@ let ``test Mutually recursive functions can be partially optimized`` () =
195206
|> Seq.concat |> Seq.map string |> String.concat ""
196207
|> equal "56"
197208

198-
// TODO: recWithFinally generates syntax error in Erlang (try/finally in recursive function)
199-
// [<Fact>]
200-
// let ``test Recursive functions containing finally work`` () =
201-
// recWithFinally () |> equal "abcdeEDCBA"
202-
203-
// TODO: recWithUse generates syntax error in Erlang (object expression IDisposable)
204-
// [<Fact>]
205-
// let ``test Recursive functions containing use work`` () =
206-
// recWithUse () |> equal "abcdeEDCBA"
209+
[<Fact>]
210+
let ``test Recursive functions containing finally work`` () =
211+
recWithFinally () |> equal "abcdeEDCBA"

tests/Beam/TypeTests.fs

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -558,14 +558,13 @@ let areEqual (x: obj) (y: obj) = x = y
558558
// and typeof comparison require runtime type information not available in Erlang.
559559
// TypeCast is erased, so :?> only works for simple casts (e.g., obj to concrete type via box/unbox).
560560

561-
// TODO: Struct types with val fields generate badmap on undefined
562-
// [<Fact>]
563-
// let ``test Value Type records work`` () =
564-
// let foo1 = ValueType<_>("foo")
565-
// let foo2 = ValueType<_>("foo")
566-
// foo1.Value |> equal "foo"
567-
// foo1.value |> equal "foo"
568-
// foo1 = foo2 |> equal true
561+
[<Fact>]
562+
let ``test Value Type records work`` () =
563+
let foo1 = ValueType<_>("foo")
564+
let foo2 = ValueType<_>("foo")
565+
foo1.Value |> equal "foo"
566+
foo1.value |> equal "foo"
567+
foo1 = foo2 |> equal true
569568

570569
[<Fact>]
571570
let ``test Value Type unions work`` () =
@@ -579,21 +578,19 @@ let ``test Value Type tuples work`` () =
579578
let tu2 = struct ("a", "b")
580579
tu1 = tu2 |> equal true
581580

582-
// TODO: Struct types with val fields generate badmap on undefined
583-
// [<Fact>]
584-
// let ``test Value Types work`` () =
585-
// let bar1 = ValueType1("bar")
586-
// let bar2 = ValueType1("bar")
587-
// bar1.Value |> equal "bar"
588-
// bar1 = bar2 |> equal true
581+
[<Fact>]
582+
let ``test Value Types work`` () =
583+
let bar1 = ValueType1("bar")
584+
let bar2 = ValueType1("bar")
585+
bar1.Value |> equal "bar"
586+
bar1 = bar2 |> equal true
589587

590-
// TODO: Struct types with val fields generate badmap on undefined
591-
// [<Fact>]
592-
// let ``test Other Value Types work`` () =
593-
// let test2 = ValueType2(3, 4)
594-
// test2.Value |> equal 7
595-
// let p = Point2D(2.)
596-
// p.Y |> equal 2.
588+
[<Fact>]
589+
let ``test Other Value Types work`` () =
590+
let test2 = ValueType2(3, 4)
591+
test2.Value |> equal 7
592+
let p = Point2D(2.)
593+
p.Y |> equal 2.
597594

598595
[<Fact>]
599596
let ``test Custom F# exceptions work`` () =

0 commit comments

Comments
 (0)