Skip to content

Commit 882574b

Browse files
a bit of work on variable length encoding
* adding expecto (+fscheck, not used yet) * expose a `runParser` function * add a (failing) test for `getVarlen` (sample values taken from https://www.csie.ntu.edu.tw/~r92092/ref/midi/ "Variable Length Quantities")
1 parent 401420b commit 882574b

File tree

9 files changed

+298
-33
lines changed

9 files changed

+298
-33
lines changed

paket.dependencies

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
source https://www.nuget.org/api/v2
22

3-
nuget System.Memory
3+
nuget System.Memory
4+
nuget Expecto.FsCheck

paket.lock

Lines changed: 154 additions & 15 deletions
Large diffs are not rendered by default.

src/ZMidi/Internal/ParserMonad.fs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,48 @@ module ParserMonad =
3030
type State =
3131
{ Position: Pos
3232
RunningStatus: VoiceEvent
33+
LastParse : obj
3334
}
34-
35-
type ParseError = ParseError of position: Pos * message: ErrMsg
35+
static member initial = { Position = 0; RunningStatus = VoiceEvent.StatusOff; LastParse = null }
36+
type ParseError =
37+
ParseError of
38+
position: Pos
39+
* message: ErrMsg
40+
#if DEBUG
41+
* lastToken : obj // need top level type, picking System.Object for now
42+
#endif
43+
44+
let inline mkOtherParseError st (genMessage : Pos -> string) =
45+
ParseError(
46+
st.Position
47+
, Other (genMessage st.Position)
48+
#if DEBUG
49+
, st.LastParse
50+
#endif
51+
)
52+
53+
let inline mkParseError st (errMsg: ErrMsg) =
54+
ParseError(
55+
st.Position
56+
, errMsg
57+
#if DEBUG
58+
, st.LastParse
59+
#endif
60+
)
3661

3762
type ParserMonad<'a> =
3863
ParserMonad of (MidiData -> State -> Result<'a * State, ParseError> )
3964

4065
let inline private apply1 (parser : ParserMonad<'a>)
4166
(midiData : byte[])
4267
(state : State) : Result<'a * State, ParseError> =
43-
let (ParserMonad fn) = parser in fn midiData state
68+
let (ParserMonad fn) = parser
69+
let result = fn midiData state
70+
match result with
71+
| Ok (r, state) ->
72+
let state = { state with LastParse = r }
73+
Ok (r, state)
74+
| Error e -> Error e
4475

4576
let inline mreturn (x:'a) : ParserMonad<'a> =
4677
ParserMonad <| fun _ st -> Ok (x, st)
@@ -53,7 +84,7 @@ module ParserMonad =
5384
| Ok (ans, st1) -> apply1 (next ans) input st1
5485

5586
let mzero () : ParserMonad<'a> =
56-
ParserMonad <| fun _ state -> Error (ParseError(state.Position, EOF "mzero"))
87+
ParserMonad <| fun _ state -> Error (mkParseError state (EOF "mzero"))
5788

5889
let inline mplus (parser1 : ParserMonad<'a>) (parser2 : ParserMonad<'a>) : ParserMonad<'a> =
5990
ParserMonad <| fun input state ->
@@ -92,26 +123,28 @@ module ParserMonad =
92123

93124
let (parseMidi:ParserBuilder) = new ParserBuilder()
94125

126+
let runParser (ma:ParserMonad<'a>) input initialState =
127+
apply1 ma input initialState
128+
|> Result.map fst
129+
95130
/// Run the parser on a file.
96131
let runParseMidi (ma : ParserMonad<'a>) (inputPath : string) : Result<'a, ParseError> =
97-
let input = File.ReadAllBytes inputPath
98-
match apply1 ma input { Position = 0; RunningStatus = StatusOff} with
99-
| Ok (ans, _) -> Ok ans
100-
| Error msg -> Error msg
132+
runParser ma (File.ReadAllBytes inputPath) State.initial
133+
101134

102135
/// Throw a parse error
103136
let parseError (genMessage : Pos -> string) : ParserMonad<'a> =
104-
ParserMonad <| fun _ st -> Error (ParseError(st.Position, Other (genMessage st.Position)))
137+
ParserMonad <| fun _ st -> Error (mkOtherParseError st genMessage)
105138

106139
/// Run the parser, if it fails swap the error message.
107140
let ( <??> ) (parser : ParserMonad<'a>) (genMessage : Pos -> string) : ParserMonad<'a> =
108141
ParserMonad <| fun input st ->
109142
match apply1 parser input st with
110143
| Ok result -> Ok result
111-
| Error _ -> Error (ParseError(st.Position, Other (genMessage st.Position)))
144+
| Error _ -> Error(mkOtherParseError st genMessage)
112145

113146
let fatalError err =
114-
ParserMonad <| fun _ st -> Error (ParseError(st.Position, err))
147+
ParserMonad <| fun _ st -> Error (mkParseError st err)
115148

116149
let getRunningEvent : ParserMonad<VoiceEvent> =
117150
ParserMonad <| fun _ st -> Ok (st.RunningStatus , st)
@@ -134,9 +167,9 @@ module ParserMonad =
134167
try
135168
match input,state with
136169
| PositionValid -> f input state
137-
| PositionInvalid -> Error (ParseError(state.Position, EOF name))
170+
| PositionInvalid -> Error (mkParseError state (EOF name))
138171
with
139-
| e -> Error (ParseError(state.Position, (Other (sprintf "%A" e))))
172+
| e -> Error (mkParseError state (Other (sprintf "%A" e)))
140173
)
141174

142175
let peek : ParserMonad<byte> =

src/ZMidi/Read.fs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module ReadFile =
77
//let midiFile : Parser = ()
88
open ZMidi.Internal.ParserMonad
99
open ZMidi.Internal.Utils
10-
10+
open ZMidi.Internal.ExtraTypes
1111

1212
/// Apply parse then apply the check, if the check fails report
1313
/// the error message.
@@ -27,7 +27,10 @@ module ReadFile =
2727
let inline clearBit (bit: int) (i: ^T) =
2828
let mask = ~~~ (LanguagePrimitives.GenericOne <<< bit)
2929
i &&& mask
30-
30+
let inline msbHigh i =
31+
match i with
32+
| TestBit 7 -> true
33+
| _ -> false
3134
let assertString (s: string) =
3235
postCheck (readString s.Length) ((=) s) (Other (sprintf "assertString: expected '%s'" s))
3336

@@ -36,10 +39,24 @@ module ReadFile =
3639

3740
let assertWord8 i =
3841
postCheck readByte ((=) i) (Other (sprintf "assertWord8: expected '%i'" i))
39-
42+
4043
let getVarlen : ParserMonad<word32> =
4144
parseMidi {
42-
failwith "getVarlen: not imple"
45+
let! a = readByte
46+
if msbHigh a then
47+
let! b = readByte
48+
if msbHigh b then
49+
let! c = readByte
50+
if msbHigh c then
51+
let! d = readByte
52+
return fromVarlen (V4(a,b,c,d))
53+
else
54+
return fromVarlen (V3(a,b,c))
55+
else
56+
return fromVarlen (V2(a, b))
57+
else
58+
return fromVarlen (V1 a)
59+
4360
}
4461

4562
let getVarlenText = gencount getVarlen readChar (fun _ b -> System.String b)

src/ZMidi/zmidi-fs-core.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
</ItemGroup>
99
<ItemGroup>
1010
<Compile Include="DataTypes.fs" />
11+
<Compile Include="ExtraTypes.fs" />
1112
<Compile Include="Internal\Utils.fs" />
1213
<Compile Include="Internal\ParserMonad.fs" />
1314
<Compile Include="Read.fs" />

test/tests/Tests.fs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module ZMidi.Tests
2+
open Expecto
3+
open ZMidi.Internal.ParserMonad
4+
open ZMidi.ReadFile
5+
6+
let tests =
7+
test "parseVarlen" {
8+
let cases =
9+
[|
10+
{| input = [| 0x00uy |]; expected = 0u |}
11+
{| input = [| 0x7fuy |]; expected = 127u |}
12+
{| input = [| 0x80uy |]; expected = 128u |}
13+
{| input = [| 0x03uy; 0xe8uy |]; expected = 1000u |}
14+
{| input = [| 0x3fuy; 0xffuy |]; expected = 16383u |}
15+
{| input = [| 0x0fuy; 0x42uy; 0x40uy |]; expected = 100000u |}
16+
|]
17+
18+
let state = State.initial
19+
20+
let failures =
21+
[|
22+
for case in cases do
23+
24+
let result = runParser getVarlen case.input state
25+
if result <> (Ok (case.expected)) then
26+
yield case, result
27+
|]
28+
29+
if failures.Length > 0 then
30+
let message =
31+
sprintf "%i failure(s):%s"
32+
failures.Length
33+
(
34+
System.Environment.NewLine
35+
+ (failures
36+
|> Array.map
37+
(fun (e, result) ->
38+
sprintf "exptected: %0x for %s, got %A"
39+
e.expected
40+
(e.input |> Array.map (sprintf "%0x") |> String.concat " ")
41+
result
42+
) |> String.concat System.Environment.NewLine
43+
44+
)
45+
)
46+
failwithf "%s" message
47+
}
48+
49+
50+
[<EntryPoint>]
51+
let main args =
52+
runTestsWithArgs defaultConfig args tests

test/tests/paket.references

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Expecto.FsCheck

test/tests/tests.fsproj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<TargetFrameworks>netcoreapp2.0;net462</TargetFrameworks>
5+
<OutputPath>..\..\build\$(Configuration)\$(Platform)</OutputPath>
6+
<OutputType>Exe</OutputType>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<Compile Include="Tests.fs" />
10+
</ItemGroup>
11+
<ItemGroup>
12+
<ProjectReference Include="..\..\src\ZMidi\zmidi-fs-core.fsproj" />
13+
</ItemGroup>
14+
<Import Project="..\..\.paket\Paket.Restore.targets" />
15+
</Project>

zmidi-fs-core.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.29001.49
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "zmidi-fs-core", "src\ZMidi\zmidi-fs-core.fsproj", "{18EE5ED6-5B88-423F-8066-93F875E8480B}"
77
EndProject
8+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "tests", "test\tests\tests.fsproj", "{B7EFA64D-6859-41B2-8259-7F63F477E6C8}"
9+
EndProject
810
Global
911
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1012
Debug|Any CPU = Debug|Any CPU
@@ -15,6 +17,10 @@ Global
1517
{18EE5ED6-5B88-423F-8066-93F875E8480B}.Debug|Any CPU.Build.0 = Debug|Any CPU
1618
{18EE5ED6-5B88-423F-8066-93F875E8480B}.Release|Any CPU.ActiveCfg = Release|Any CPU
1719
{18EE5ED6-5B88-423F-8066-93F875E8480B}.Release|Any CPU.Build.0 = Release|Any CPU
20+
{B7EFA64D-6859-41B2-8259-7F63F477E6C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{B7EFA64D-6859-41B2-8259-7F63F477E6C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{B7EFA64D-6859-41B2-8259-7F63F477E6C8}.Release|Any CPU.ActiveCfg = Release|Any CPU
23+
{B7EFA64D-6859-41B2-8259-7F63F477E6C8}.Release|Any CPU.Build.0 = Release|Any CPU
1824
EndGlobalSection
1925
GlobalSection(SolutionProperties) = preSolution
2026
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)