Get floating point numbers working too

This commit is contained in:
Reid 'arrdem' McKenzie 2020-07-18 13:46:10 -06:00
parent d60e690445
commit d7f175a987
4 changed files with 174 additions and 49 deletions

View file

@ -0,0 +1,83 @@
(defpackage com.twitter.wilson
(require
;; The log lets you record status information into a program's trace
[flowmetal.log
:refer [log!]]
;; The time system lets you put bounds on the implicit awaiting Flowmetal does
[flowmetal.time
:refer [with-timeout!, timeout?, make-duration, duration?, +seconds+, +hours+, sleep!]]
;; JSON. Simple enough
[flowmetal.json
:refer [loads, dumps, json?]]
;; Extensions! Provided by other systems.
;;
;; This one allows for an external service to receive HTTP callbacks on Flowmetal's behalf.
[http.callback
:refer [make-callback!, get-callback!, callback?]]
;; This one allows for an external service to make HTTP requests on Flowmetal's behalf.
[http.request
:refer [post!, error?, dns-error?, connection-error?, response-error?]])
(defenum stage
+reboot+
+bios-update+
+reinstall+)
;; FIXME:- how to do table optimization?
(defn fib [x]
(match x
[0 1]
[1 1]
[_ (+ (fib (- x 1) (- x 2)))]))
(defn retry-http [f
:- (fn? [] a?)
backoff-fn
:- (fn? [int?] duration?)
:default (fn [x :- int?]
:- duration?
(make-duration (fib x) +seconds+))
backoff-count
:- int?
:default 0]
:- a
"The implementation of HTTP with retrying."
(let [response (f)]
(if (not (error? response))
response
;; FIXME:- how does auth denied get represented?
(if (or (dns-error? response)
(connection-error? response)
(response-error? response))
(do (sleep (backoff-fn backoff-count))
(retry-http* f backoff-fn (+ backoff-count 1)))))))
(defn job [hostname
:- str?
stages
:- (list? stage?)
job-timeout
:- duration?
:default (duration 3 :hours)]
:- (union? [timeout? json?])
"Run a wilson job, wait for the callback and process the result.
By default the job is only waited for three hours.
"
(let [callback :- callback? (make-callback!)
job (retry-http
(fn []
(post "http://wilson.local.twitter.com"
:data
(dumps
{:host hostname
:stages [stages]
:callbacks [{:type :http, :url callback}]}))))]
(let [result (with-timeout! (duration 3 :hours)
(fn []
(get-callback callback)))]
(if-not (timeout? result)
(loads result)
result)))))

View file

@ -7,6 +7,7 @@ from enum import Enum
from io import StringIO, BufferedReader from io import StringIO, BufferedReader
from typing import IO, NamedTuple from typing import IO, NamedTuple
from fractions import Fraction from fractions import Fraction
import re
## Types ## Types
@ -54,9 +55,9 @@ class IntegerToken(NamedTuple, TokenBase):
return return
class RationalToken(NamedTuple, TokenBase): class FractionToken(NamedTuple, TokenBase):
"""A read integer, including position.""" """A read fraction, including position."""
data: int data: Fraction
raw: str raw: str
pos: Position pos: Position
@ -66,7 +67,7 @@ class RationalToken(NamedTuple, TokenBase):
class FloatToken(NamedTuple, TokenBase): class FloatToken(NamedTuple, TokenBase):
"""A read floating point number, including position.""" """A read floating point number, including position."""
data: int data: float
raw: str raw: str
pos: Position pos: Position
@ -225,8 +226,6 @@ class Parser(SexpParser):
return cls.parse_sqlist(f) return cls.parse_sqlist(f)
elif cls.isspace(f.peek()): elif cls.isspace(f.peek()):
return cls.parse_whitespace(f) return cls.parse_whitespace(f)
elif f.peek().isdigit():
return cls.parse_num(f)
elif f.peek() == ";": elif f.peek() == ";":
return cls.parse_comment(f) return cls.parse_comment(f)
else: else:
@ -235,7 +234,21 @@ class Parser(SexpParser):
@classmethod @classmethod
def isspace(cls, ch: str): def isspace(cls, ch: str):
"""An extension point allowing for a more expansive concept of whitespace.""" """An extension point allowing for a more expansive concept of whitespace."""
return ch.isspace() return ch.isspace() or ch == ','
@classmethod
def ispunct(cls, ch: str):
return cls.isspace(ch) or ch in (
';' # Semicolon
'()' # Parens
'⟮⟯' # 'flat' parens
'[]' # Square brackets
'⟦⟧' # 'white' square brackets
'{}' # Curly brackets
'⟨⟩' # Angle brackets
'《》' # Double angle brackets
'⟪⟫' # Another kind of double angle brackets
)
@classmethod @classmethod
def parse_delimeted(cls, f: PosTrackingBufferedReader, openc, closec, ctor): def parse_delimeted(cls, f: PosTrackingBufferedReader, openc, closec, ctor):
@ -260,57 +273,41 @@ class Parser(SexpParser):
return cls.parse_delimeted(f, "[", "]", lambda *args: ListToken(*args, ListType.SQUARE)) return cls.parse_delimeted(f, "[", "]", lambda *args: ListToken(*args, ListType.SQUARE))
@classmethod @classmethod
def parse_unum(cls, f: PosTrackingBufferedReader): def handle_symbol(cls, buff, pos):
with ReadThroughBuffer(f) as rtb: # Parsing integers
assert rtb.peek().isdigit() if m := re.fullmatch(r"[+-]?\d[\d_]*", buff):
pos = f.pos() return IntegerToken(int(buff.replace("_", "")), buff, pos)
while rtb.peek().isdigit():
rtb.read()
buff = str(rtb)
return IntegerToken(int(buff), buff, pos)
@classmethod # Parsing integers with bases
def parse_num(cls, f: PosTrackingBufferedReader): elif m := re.fullmatch(r"(\d+)r([a-z0-9_]+)", buff):
with ReadThroughBuffer(f) as rtb: return IntegerToken(int(m.group(2).replace("_", "")), int(m.group(1)), buff, pos)
num: IntegerToken = cls.parse_unum(rtb)
# Various cases of more interesting numbers # Parsing fractions
if rtb.peek() == "/": elif m := re.fullmatch(r"([+-]?\d[\d_]*)/(\d[\d_]*)", buff):
## Case of a rational return FractionToken(
# Discard the delimeter Fraction(
rtb.read() int(m.group(1).replace("_", "")),
denom = cls.parse_num(rtb) int(m.group(2).replace("_", ""))),
buff,
pos,
)
return RationalToken(Fraction(num.data, denom.data), str(rtb), num.pos) # Parsing floats
elif re.fullmatch(r"[+-]?\d[\d_]*(\.\d[\d_]*)?(e[+-]?\d[\d_]*)?", buff):
return FloatToken(float(buff), buff, pos)
elif rtb.peek() == "r": else:
## Case of a number with a base return SymbolToken(buff, buff, pos)
# Discard thd delimeter
rtb.read()
body = cls.parse_symbol(rtb)
return IntegerToken(int(body.raw, num.data), str(rtb), num.pos)
elif rtb.peek() == ".":
## Case of a number with a decimal component
## Note there may be a trailing exponent
raise NotImplementedError()
elif rtb.peek() == "e":
## Case of a number with a floating point exponent
raise NotImplementedError()
else:
return num
@classmethod @classmethod
def parse_symbol(cls, f: PosTrackingBufferedReader): def parse_symbol(cls, f: PosTrackingBufferedReader):
with ReadThroughBuffer(f) as rtb: with ReadThroughBuffer(f) as rtb:
pos = None pos = None
while rtb.peek() and not cls.isspace(rtb.peek()): while rtb.peek() and not cls.ispunct(rtb.peek()):
pos = pos or rtb.pos() pos = pos or rtb.pos()
rtb.read() rtb.read()
buff = str(rtb) buff = str(rtb)
return SymbolToken(buff, buff, pos) return cls.handle_symbol(buff, pos)
@classmethod @classmethod
def parse_whitespace(cls, f: PosTrackingBufferedReader): def parse_whitespace(cls, f: PosTrackingBufferedReader):

View file

@ -12,7 +12,7 @@ import flowmetal.parser as p
### Types ### Types
## We are not, in fact, sponsored by Typelevel LLC. ## We are not, in fact, sponsored by Typelevel LLC.
class TypeLevelExpr(ABC): class TypeLevelExpr(object):
"""A base class for type-level expressions.""" """A base class for type-level expressions."""
pass pass
@ -27,24 +27,29 @@ class TypeExpr(TypeLevelExpr, NamedTuple):
## Now down to reality ## Now down to reality
class ValueLevelExpr(ABC): class ValueLevelExpr(object):
"""A base class for value-level expressions.""" """A base class for value-level expressions."""
pass
class AscribeExpr(ValueLevelExpr, NamedTuple): class AscribeExpr(ValueLevelExpr, NamedTuple):
"""Ascribe a type (via type-level expression) to a value-level expression.""" """Ascribe a type (via type-level expression) to a value-level expression."""
pass
class InvokeExpr(ValueLevelExpr, NamedTuple): class InvokeExpr(ValueLevelExpr, NamedTuple):
"""(a ⊢ (fn A ⊢ B) [...] ⊢ A) ⊢ B""" """(a ⊢ (fn A ⊢ B) [...] ⊢ A) ⊢ B"""
pass
class IfExpr(ValueLevelExpr, NamedTuple): class IfExpr(ValueLevelExpr, NamedTuple):
"""(if test a ⊢ A b ⊢ B) ⊢ (Variant A B).""" """(if test a ⊢ A b ⊢ B) ⊢ (Variant A B)."""
pass
class LetExpr(ValueLevelExpr, NamedTuple): class LetExpr(ValueLevelExpr, NamedTuple):
"""Let a single binding and wrap a body. Yes one. N-ary let is an abstraction.""" """Let a single binding and wrap a body. Yes one. N-ary let is an abstraction."""
pass
class DoExpr(ValueError, NamedTuple): class DoExpr(ValueError, NamedTuple):
@ -52,24 +57,31 @@ class DoExpr(ValueError, NamedTuple):
(do a b c ... ω Ω) Ω (do a b c ... ω Ω) Ω
""" """
pass
ProcExpr = DoExpr # ain't broke don't fix it ProcExpr = DoExpr # ain't broke don't fix it
class MappingExpr(ValueLevelExpr, NamedTuple): class MappingExpr(ValueLevelExpr, NamedTuple):
"""Mappings require their own constructor expression due to local/symbol references.""" """Mappings require their own constructor expression due to local/symbol references."""
pass
class SetExpr(ValueLevelExpr, NamedTuple): class SetExpr(ValueLevelExpr, NamedTuple):
"""Sets require their own constructor expression due to local/symbol references.""" """Sets require their own constructor expression due to local/symbol references."""
pass
class ListExpr(ValueLevelExpr, NamedTuple): class ListExpr(ValueLevelExpr, NamedTuple):
"""While round () lists are generally InvokeExprs, [] lists are constructors like sets and maps.""" """While round () lists are generally InvokeExprs, [] lists are constructors like sets and maps."""
pass
## Reader implementation ## Reader implementation
class SexpAnalyzer(ABC): class SexpAnalyzer(ABC):
"""A base class for Analyzers.""" """A base class for Analyzers."""
pass
class Analyzer(SexpAnalyzer): class Analyzer(SexpAnalyzer):

View file

@ -2,6 +2,8 @@
Tests covering the Flowmetal parser. Tests covering the Flowmetal parser.
""" """
from math import nan
import flowmetal.parser as p import flowmetal.parser as p
import pytest import pytest
@ -27,7 +29,7 @@ def test_parse_num(num):
]) ])
def test_parse_ratio(frac): def test_parse_ratio(frac):
"""Test covering the ratio notation.""" """Test covering the ratio notation."""
assert isinstance(p.parses(frac), p.RationalToken) assert isinstance(p.parses(frac), p.FractionToken)
assert p.parses(frac).data == p.Fraction(frac) assert p.parses(frac).data == p.Fraction(frac)
@ -64,3 +66,34 @@ def test_list_contents(txt, tokenization):
for (type, text), token in zip(tokenization, lelems): for (type, text), token in zip(tokenization, lelems):
assert isinstance(token, type) assert isinstance(token, type)
assert token.raw == text assert token.raw == text
@pytest.mark.parametrize('txt, value', [
('1.0', 1.0),
('-1.0', -1.0),
('1.01', 1.01),
('1e0', 1e0),
('1e3', 1e3),
('1e-3', 1e-3),
('1.01e3', 1.01e3),
])
def test_float_values(txt, value):
"""Some examples of floats."""
assert isinstance(p.parses(txt), p.FloatToken)
assert p.parses(txt).data == value
@pytest.mark.parametrize('txt, tokenization', [
('+1', p.IntegerToken),
('+1+', p.SymbolToken),
('+1e', p.SymbolToken),
('+1e3', p.FloatToken),
('+1.0', p.FloatToken),
('+1.0e3', p.FloatToken),
('a.b', p.SymbolToken),
('1.b', p.SymbolToken),
])
def test_ambiguous_floats(txt, tokenization):
"""Parse examples of 'difficult' floats and symbols."""
assert isinstance(p.parses(txt), tokenization), "Token type didn't match!"
assert p.parses(txt).raw == txt, "Parse wasn't total!"