Time lib and http

This commit is contained in:
allexanderbergmns
2026-08-25 15:39:44 +02:00
parent 4ccc8a0f95
commit 5daadce7a9
11 changed files with 310 additions and 7 deletions
+21
View File
@@ -157,6 +157,23 @@ Build builtins:
`examples/build.vrmm` for a tour (targets: `main`, `multi`, `test`, `bench`, `examples/build.vrmm` for a tour (targets: `main`, `multi`, `test`, `bench`,
`deploy`, `clean`). `deploy`, `clean`).
## Standard library modules
The toolchain ships a small stdlib in `lib/`, imported by name like in V
(`import os` resolves against `lib/` and `vendor/` from any directory).
Module functions are namespaced: `os.exists()`, `json.encode()`, ...
| module | functions |
| --- | --- |
| `os` | filesystem + process: `exists`, `is_dir`, `is_file`, `mkdir`, `remove`, `copy`, `list_dir`, `glob`, `join`, `base`, `dir`, `read_lines`, `write_lines`, `cwd`, `env`, `exec` |
| `json` | `encode`, `decode`, `pretty` — objects become structs, integral numbers decode as ints, `null``none` |
| `strings` | `length`, `lines`, `split`, `join`, `replace`, `contains`, `starts_with`, `ends_with`, `upper`, `lower`, `trim`, `pad`, `pad_left`, `repeat`, `format`, `capitalize` |
| `math` | `abs`, `min`, `max`, `floor`, `ceil`, `round`, `sqrt`, `pow`, `clamp`, `sign`, `pi` |
| `http` | `get(url)`, `post(url, data)`, `get_text(url)` — return `{status, body}`; network failures throw (catchable), HTTP errors (404) are normal responses |
| `time` | `now()` (epoch s), `ms()` (epoch ms), `format(t, "YYYY-MM-DD HH:mm:ss")`, `date(t)`, `clock(t)`, `parse("2026-08-25 13:36:45")` — Moment-style tokens |
Examples: `examples/imports.vr`, `examples/http.vr`, `examples/time.vr`.
## The VuurRaaf language ## The VuurRaaf language
A small, V-flavored language. Values are 64-bit integers, 64-bit floats, A small, V-flavored language. Values are 64-bit integers, 64-bit floats,
@@ -260,6 +277,10 @@ fn main() {
- bitwise operators: `& | ^ ~ << >>` - bitwise operators: `& | ^ ~ << >>`
- host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`, - host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`,
`exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join` `exit`, `sleep`, `time()`, `type(x)`, `str(x)`, `int(x)`, `split` / `join`
- HTTP: `http_get(url)` / `http_post(url, data)` return `{status, body}`
structs and throw on network failure (catchable) — or use `import http`
- date/time: `now()` (epoch s), `time_ms()`, `format_time(t, "YYYY-MM-DD")`,
`parse_time(s)` (ISO timestamps) — or use `import time`
- JSON: `json_encode(x)` / `json_decode(s)` — objects become structs, arrays - JSON: `json_encode(x)` / `json_decode(s)` — objects become structs, arrays
become arrays, integral numbers decode as ints, `null` decodes to `none` become arrays, integral numbers decode as ints, `null` decodes to `none`
- `none`: a literal for "no value" (JSON null); `x == none` compares, - `none`: a literal for "no value" (JSON null); `x == none` compares,
+16 -1
View File
@@ -232,12 +232,17 @@ fn (mut c Checker) check_stmt(st Stmt) ! {
c.loop_depth-- c.loop_depth--
} }
.for_in_stmt { .for_in_stmt {
// for x in EnumName { ... } iterates the enum's variants
if st.expr.kind == .ident && st.expr.name in c.enums {
c.types[st.target] = TypeInfo{ kind: .enum_t, name: st.expr.name }
} else {
seq := c.check_expr(st.expr)! seq := c.check_expr(st.expr)!
// iterate enums and arrays; unknown is allowed (dynamic) // iterate enums and arrays; unknown is allowed (dynamic)
if seq.kind == .int_t || seq.kind == .float_t || seq.kind == .bool_t || seq.kind == .none_t { if seq.kind == .int_t || seq.kind == .float_t || seq.kind == .bool_t || seq.kind == .none_t {
return error('cannot iterate a ${type_name(seq.kind)} (line ${st.line})') return error('cannot iterate a ${type_name(seq.kind)} (line ${st.line})')
} }
c.types[st.target] = TypeInfo{ kind: .unknown } c.types[st.target] = TypeInfo{ kind: .unknown }
}
if st.idx_target.len > 0 { if st.idx_target.len > 0 {
c.types[st.idx_target] = TypeInfo{ kind: .int_t } c.types[st.idx_target] = TypeInfo{ kind: .int_t }
} }
@@ -328,9 +333,14 @@ fn (mut c Checker) check_expr(e Expr) !TypeInfo {
TypeInfo{ kind: .unknown } TypeInfo{ kind: .unknown }
} }
.field { .field {
// enum variant: Color.red — the base is the enum type name, not a
// variable, so it must be resolved before check_expr on the base
if e.left.kind == .ident && e.left.name in c.enums {
return TypeInfo{ kind: .enum_t, name: e.left.name }
}
base := c.check_expr(*e.left)! base := c.check_expr(*e.left)!
c.expect_struct_like(base, 'field access', e.line)! c.expect_struct_like(base, 'field access', e.line)!
// enum variant: Color.red → enum_t // enum variant on an enum-typed receiver: Color.red → enum_t
if base.kind == .enum_t { if base.kind == .enum_t {
return TypeInfo{ kind: .enum_t, name: base.name } return TypeInfo{ kind: .enum_t, name: base.name }
} }
@@ -599,6 +609,11 @@ fn builtin_result_type(name string) TypeInfo {
'build_glob', 'build_ls' { TypeInfo{ kind: .array_t } } 'build_glob', 'build_ls' { TypeInfo{ kind: .array_t } }
'build_run', 'build_test', 'build_bench', 'build_clean', 'build_exec_status', 'build_run', 'build_test', 'build_bench', 'build_clean', 'build_exec_status',
'build_exists', 'build_mkdir', 'build_rm', 'build_copy' { TypeInfo{ kind: .int_t } } 'build_exists', 'build_mkdir', 'build_rm', 'build_copy' { TypeInfo{ kind: .int_t } }
// HTTP client: returns a {status, body} struct
'http_get', 'http_post' { TypeInfo{ kind: .struct_t } }
// date/time
'now', 'time_ms', 'parse_time' { TypeInfo{ kind: .int_t } }
'format_time' { TypeInfo{ kind: .string_t } }
else { TypeInfo{ kind: .unknown } } else { TypeInfo{ kind: .unknown } }
} }
} }
+8
View File
@@ -974,6 +974,14 @@ fn builtin_spec(name string) (int, int) {
'build_is_dir' { native_build_is_dir, 1 } 'build_is_dir' { native_build_is_dir, 1 }
'cwd' { native_cwd, 0 } 'cwd' { native_cwd, 0 }
'json_pretty' { native_json_pretty, 1 } 'json_pretty' { native_json_pretty, 1 }
// HTTP client
'http_get' { native_http_get, 1 }
'http_post' { native_http_post, 2 }
// date/time
'now' { native_now, 0 }
'time_ms' { native_time_ms, 0 }
'format_time' { native_format_time, 2 }
'parse_time' { native_parse_time, 1 }
else { -1, 0 } else { -1, 0 }
} }
} }
+10
View File
@@ -141,3 +141,13 @@ const native_repeat = 164
const native_build_is_dir = 165 const native_build_is_dir = 165
const native_cwd = 166 const native_cwd = 166
const native_json_pretty = 167 const native_json_pretty = 167
// HTTP client builtins
const native_http_get = 168
const native_http_post = 169
// date/time builtins
const native_now = 170
const native_time_ms = 171
const native_format_time = 172
const native_parse_time = 173
+34
View File
@@ -0,0 +1,34 @@
// http.vr — the HTTP client (needs network access).
// vr run examples/http.vr
//
// http.get / http.post return a struct with `status` (int) and `body`
// (string). Network failures throw and can be caught with try/catch;
// HTTP-level errors (404, 500) come back as normal responses.
import http
import json
fn main() {
let res = http.get("https://httpbin.org/get")
println("status: " + str(res.status))
let data = json.decode(res.body)
println("your IP (from httpbin): " + str(data.origin))
let post = http.post("https://httpbin.org/post", "name=vuurraaf")
let echo = json.decode(post.body)
println("echoed: " + str(echo.data))
// a 404 is a response, not an exception
let nf = http.get("https://httpbin.org/status/404")
println("not found status: " + str(nf.status))
// network failures are catchable
try {
http.get("http://127.0.0.1:1/nope")
println("unreachable")
} catch e {
println("caught connection error")
}
println("done")
}
+75 -1
View File
@@ -5,6 +5,15 @@ import os
import json import json
import strings import strings
import math import math
import http
import time
enum Suit {
hearts
diamonds
clubs
spades
}
fn add(a, b) { fn add(a, b) {
return a + b return a + b
@@ -310,7 +319,7 @@ fn test_module_imports() {
} }
assert os.base("a/b/c.vr") == "c.vr" assert os.base("a/b/c.vr") == "c.vr"
assert os.dir("a/b/c.vr") == "a/b" assert os.dir("a/b/c.vr") == "a/b"
assert len(os.glob("lib/*.vr")) == 4 assert len(os.glob("lib/*.vr")) == 6
let tmp = os.join(os.cwd(), "tmp_test.txt") let tmp = os.join(os.cwd(), "tmp_test.txt")
os.write_lines(tmp, ["x", "y"]) os.write_lines(tmp, ["x", "y"])
assert len(os.read_lines(tmp)) == 2 assert len(os.read_lines(tmp)) == 2
@@ -381,6 +390,71 @@ fn test_closures() {
assert tag("warn") == "[warn]" assert tag("warn") == "[warn]"
} }
fn test_enums() {
// variant values
assert Suit.hearts == 0
assert Suit.spades == 3
// enum.count()
assert Suit.hearts.count() == 4
// enum.to_string()
assert Suit.spades.to_string() == "spades"
// enums in expressions
let next = Suit.hearts + 1
assert next == 1
// enum iteration
let names = []
for s in Suit {
names = push(names, s.to_string())
}
assert len(names) == 4
assert names[0] == "hearts"
assert names[3] == "spades"
// match on enum values
assert suit_code(Suit.clubs) == "c"
assert suit_code(Suit.hearts) == "h"
}
fn suit_code(s) {
match s {
Suit.hearts { return "h" }
Suit.clubs { return "c" }
else { return "?" }
}
}
fn test_http_errors() {
// offline-safe: connecting to a closed local port fails fast
try {
http.get("http://127.0.0.1:1/nope")
assert false
} catch e {
assert len(e) > 0
}
}
fn test_time() {
let t = time.now()
assert t > 1700000000
// format produces fixed-width strings
let day = time.date(t)
assert len(day) == 10
assert day[4] == "-"[0]
let full = time.format(t, "YYYY-MM-DD HH:mm:ss")
assert len(full) == 19
// parse round-trips within a second
let back = time.parse(full)
assert abs(back - t) <= 1
// ms is more precise than seconds
assert time.ms() > t * 1000
// invalid input throws
try {
time.parse("not a date")
assert false
} catch e {
assert len(e) > 0
}
}
fn test_failing() { fn test_failing() {
// this one is meant to fail — shows up in `vr test` output // this one is meant to fail — shows up in `vr test` output
assert 1 == 2 assert 1 == 2
+29
View File
@@ -0,0 +1,29 @@
// time.vr — date/time helpers.
// vr run examples/time.vr
//
// Tokens follow the Moment.js style: YYYY, MM, DD, HH, mm, ss, with
// literals like "-" and ":". All values are Unix epoch seconds.
import time
fn main() {
let now = time.now()
println("epoch seconds: " + str(now))
println("date: " + time.date(now))
println("clock: " + time.clock(now))
println("full: " + time.format(now, "YYYY-MM-DD HH:mm:ss"))
println("custom: " + time.format(now, "MM/DD/YYYY"))
// parse a timestamp back to epoch seconds
let parsed = time.parse("2026-08-25 12:00:00")
println("parsed epoch: " + str(parsed))
// milliseconds for timing
let start = time.ms()
let x = 0
for i in 0..100000 {
x = x + i
}
let elapsed = time.ms() - start
println("loop took " + str(elapsed) + "ms (checksum " + str(x) + ")")
}
+23
View File
@@ -0,0 +1,23 @@
// http.vr — HTTP client (part of the VuurRaaf stdlib).
//
// import http
// let res = http.get("https://api.example.com/status")
// println(str(res.status))
// println(res.body)
//
// http.get / http.post return a struct with `status` (int) and `body`
// (string). Network failures (DNS, refused, timeout) throw, so they can be
// caught with try/catch; HTTP-level errors (404, 500) are normal responses.
fn get(url) {
return http_get(url)
}
fn post(url, data) {
return http_post(url, data)
}
// get_text returns just the response body as a string.
fn get_text(url) {
return http_get(url).body
}
+36
View File
@@ -0,0 +1,36 @@
// time.vr — date/time helpers (part of the VuurRaaf stdlib).
//
// import time
// println(time.format(time.now(), "YYYY-MM-DD HH:mm:ss"))
// println(time.format(time.now(), "YYYY-MM-DD"))
//
// Tokens follow the Moment.js style: YYYY (year), MM (month), DD (day),
// HH (24h hour), mm (minute), ss (second), plus literals like "-", ":".
// All values are Unix epoch seconds. `time.parse` accepts common ISO
// timestamps like "2026-08-25 14:03:22".
fn now() {
return now()
}
fn ms() {
return time_ms()
}
fn format(t, spec) {
return format_time(t, spec)
}
fn parse(s) {
return parse_time(s)
}
// date(t) formats just the calendar day: YYYY-MM-DD.
fn date(t) {
return format_time(t, "YYYY-MM-DD")
}
// clock(t) formats just the wall clock: HH:mm:ss.
fn clock(t) {
return format_time(t, "HH:mm:ss")
}
+43
View File
@@ -11,6 +11,7 @@ import os
import math import math
import rand import rand
import time import time
import net.http
import obj import obj
import compiler import compiler
import assembler import assembler
@@ -647,12 +648,54 @@ fn (mut v Vm) native(id int, _argc int) ! {
s := v.json_pretty_value(x, 0) or { return error('json_pretty: ${err.msg()}') } s := v.json_pretty_value(x, 0) or { return error('json_pretty: ${err.msg()}') }
v.push(v.alloc_str(s))! v.push(v.alloc_str(s))!
} }
// -------------------------------------------------------------------
// HTTP client
native_http_get {
url := v.pop_str()!
resp := http.get(url) or { return error('http_get: ${err.msg()}') }
v.push_http_response(resp)!
}
native_http_post {
data := v.pop_str()!
url := v.pop_str()!
resp := http.post(url, data) or { return error('http_post: ${err.msg()}') }
v.push_http_response(resp)!
}
// -------------------------------------------------------------------
// date/time
native_now {
v.push(v.enc_int(time.now().unix()))!
}
native_time_ms {
v.push(v.enc_int(time.now().unix_milli()))!
}
native_format_time {
spec := v.pop_str()!
t := v.dec_int(v.pop()!)
v.push(v.alloc_str(time.unix(t).custom_format(spec)))!
}
native_parse_time {
s := v.pop_str()!
t := time.parse(s) or { return error('parse_time: ${err.msg()}') }
v.push(v.enc_int(t.unix()))!
}
else { else {
return error('unknown native builtin ${id}') return error('unknown native builtin ${id}')
} }
} }
} }
// push_http_response wraps an HTTP response as a {status, body} struct value
// so scripts can read res.status and res.body.
fn (mut v Vm) push_http_response(resp http.Response) ! {
body_h := v.alloc_str(resp.body)
mut fields := []Field{len: 2}
fields[0] = Field{ name: 'status', val: v.enc_int(i64(resp.status_code)) }
fields[1] = Field{ name: 'body', val: body_h }
v.structs << StructVal{ fields: fields }
v.push(v.mkstruct_handle(v.structs.len - 1))!
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// format() — a small printf-style formatter: %d %i %f %s %x %X %% with // format() — a small printf-style formatter: %d %i %f %s %x %X %% with
// optional width, left-align (-), zero-padding (0), and precision (.N). // optional width, left-align (-), zero-padding (0), and precision (.N).
+10
View File
@@ -140,3 +140,13 @@ const native_repeat = 164
const native_build_is_dir = 165 const native_build_is_dir = 165
const native_cwd = 166 const native_cwd = 166
const native_json_pretty = 167 const native_json_pretty = 167
// HTTP client builtins
const native_http_get = 168
const native_http_post = 169
// date/time builtins
const native_now = 170
const native_time_ms = 171
const native_format_time = 172
const native_parse_time = 173