diff --git a/README.MD b/README.MD index bc00c6a..30aeae4 100644 --- a/README.MD +++ b/README.MD @@ -157,6 +157,23 @@ Build builtins: `examples/build.vrmm` for a tour (targets: `main`, `multi`, `test`, `bench`, `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 A small, V-flavored language. Values are 64-bit integers, 64-bit floats, @@ -260,6 +277,10 @@ fn main() { - bitwise operators: `& | ^ ~ << >>` - host builtins: `read_file` / `write_file`, `args()`, `getenv` / `setenv`, `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 become arrays, integral numbers decode as ints, `null` decodes to `none` - `none`: a literal for "no value" (JSON null); `x == none` compares, diff --git a/compiler/check.v b/compiler/check.v index fd2a101..d93fde4 100644 --- a/compiler/check.v +++ b/compiler/check.v @@ -232,12 +232,17 @@ fn (mut c Checker) check_stmt(st Stmt) ! { c.loop_depth-- } .for_in_stmt { - seq := c.check_expr(st.expr)! - // 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 { - return error('cannot iterate a ${type_name(seq.kind)} (line ${st.line})') + // 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)! + // 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 { + 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 { 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 } } .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)! 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 { 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_run', 'build_test', 'build_bench', 'build_clean', 'build_exec_status', '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 } } } } diff --git a/compiler/codegen.v b/compiler/codegen.v index 3d7d789..8d95874 100644 --- a/compiler/codegen.v +++ b/compiler/codegen.v @@ -974,6 +974,14 @@ fn builtin_spec(name string) (int, int) { 'build_is_dir' { native_build_is_dir, 1 } 'cwd' { native_cwd, 0 } '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 } } } diff --git a/compiler/opcodes.v b/compiler/opcodes.v index 7b2373f..857e43e 100644 --- a/compiler/opcodes.v +++ b/compiler/opcodes.v @@ -141,3 +141,13 @@ const native_repeat = 164 const native_build_is_dir = 165 const native_cwd = 166 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 diff --git a/examples/http.vr b/examples/http.vr new file mode 100644 index 0000000..badd810 --- /dev/null +++ b/examples/http.vr @@ -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") +} diff --git a/examples/tests.vr b/examples/tests.vr index 2c05d09..ad669b2 100644 --- a/examples/tests.vr +++ b/examples/tests.vr @@ -5,6 +5,15 @@ import os import json import strings import math +import http +import time + +enum Suit { + hearts + diamonds + clubs + spades +} fn add(a, b) { return a + b @@ -310,7 +319,7 @@ fn test_module_imports() { } assert os.base("a/b/c.vr") == "c.vr" 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") os.write_lines(tmp, ["x", "y"]) assert len(os.read_lines(tmp)) == 2 @@ -381,6 +390,71 @@ fn test_closures() { 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() { // this one is meant to fail — shows up in `vr test` output assert 1 == 2 diff --git a/examples/time.vr b/examples/time.vr new file mode 100644 index 0000000..fbd2a79 --- /dev/null +++ b/examples/time.vr @@ -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) + ")") +} diff --git a/lib/http.vr b/lib/http.vr new file mode 100644 index 0000000..30dd0e6 --- /dev/null +++ b/lib/http.vr @@ -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 +} diff --git a/lib/time.vr b/lib/time.vr new file mode 100644 index 0000000..52c9126 --- /dev/null +++ b/lib/time.vr @@ -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") +} diff --git a/vm/native.v b/vm/native.v index 0d2e338..9041900 100644 --- a/vm/native.v +++ b/vm/native.v @@ -11,6 +11,7 @@ import os import math import rand import time +import net.http import obj import compiler 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()}') } 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 { 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 // optional width, left-align (-), zero-padding (0), and precision (.N). diff --git a/vm/opcodes.v b/vm/opcodes.v index d24a644..e5952e8 100644 --- a/vm/opcodes.v +++ b/vm/opcodes.v @@ -140,3 +140,13 @@ const native_repeat = 164 const native_build_is_dir = 165 const native_cwd = 166 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