initial commit

This commit is contained in:
2026-09-25 23:32:29 +02:00
commit fff8b677f8
114 changed files with 84233 additions and 0 deletions
+873
View File
@@ -0,0 +1,873 @@
package pdf
import "core:math"
import "core:testing"
// ---------------------------------------------------------------------------
// Shared fixtures for the colour and function tests
//
// Both layers need a Document to allocate from and to resolve references
// against, and a few objects that can only be reached by reference: a function
// that names itself, a colour space named from a resource dictionary. The file
// below is opened by `cf_open`; its cross-reference table is deliberately
// absent, which the reader repairs by scanning, because what these tests need
// from it is objects, not structure.
// ---------------------------------------------------------------------------
@(private)
CF_PDF: string : "%PDF-1.7\n" +
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" +
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" +
"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 10 10] >>\nendobj\n" +
// A spot colour: full tint is 100% magenta and yellow, i.e. red.
"5 0 obj\n[/Separation /Spot /DeviceCMYK 6 0 R]\nendobj\n" +
"6 0 obj\n<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [0 1 1 0] /N 1 >>\nendobj\n" +
// A stitching function whose first sub-function is itself.
"7 0 obj\n<< /FunctionType 3 /Domain [0 1] /Functions [7 0 R 8 0 R] " +
"/Bounds [0.5] /Encode [0 1 0 1] >>\nendobj\n" +
"8 0 obj\n<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>\nendobj\n" +
// A colour space that names itself through the resource dictionary.
"9 0 obj\n/Self\nendobj\n" +
// An Indexed space with a four-entry table, reached by reference.
"10 0 obj\n[/Indexed /DeviceRGB 3 <FF0000 00FF00 0000FF FFFFFF>]\nendobj\n" +
"trailer\n<< /Size 11 /Root 1 0 R >>\n"
@(private)
cf_open :: proc(t: ^testing.T) -> ^Document {
doc, err := open_bytes(transmute([]byte)CF_PDF, Open_Options{skip_page_tree = true})
testing.expect_value(t, err, Error.None)
testing.expect(t, doc != nil, "fixture document did not open")
return doc
}
// cf_parse parses one object out of a source literal into the Document's
// arena. The literal is static, which is what lets names and strings borrow
// from it for as long as the Document lives.
@(private)
cf_parse :: proc(doc: ^Document, src: string) -> Object {
p := parser_make(doc, transmute([]byte)src, doc.allocator)
o, _ := parse_object(&p)
return o
}
@(private)
cf_bytes :: proc(s: string) -> []byte {
return transmute([]byte)s
}
// cf_stream builds a stream out of a dictionary literal and a payload. The
// payload is stored, not copied, so it must be static or arena memory.
@(private)
cf_stream :: proc(doc: ^Document, dict_src: string, payload: []byte) -> ^Stream {
d, _ := to_dict(cf_parse(doc, dict_src))
return new_stream(d, payload, doc.allocator)
}
@(private)
cf_close :: proc(t: ^testing.T, doc: ^Document) {
close(doc)
}
@(private = "file")
EPS :: 1e-9
@(private = "file")
expect_near :: proc(t: ^testing.T, got, want: f64, tol: f64, what: string,
loc := #caller_location) {
if !(abs(got - want) <= tol) {
testing.expectf(t, false, "%s: got %f, want %f", what, got, want, loc = loc)
}
}
// eval1 evaluates a one-output function at one input.
@(private = "file")
eval1 :: proc(f: ^Function, x: f64) -> f64 {
if f == nil {
return math.QNAN_F64
}
in_ := [1]f64{x}
out: [MAX_FUNCTION_OUTPUTS]f64
function_eval(f, in_[:], out[:max(f.n_out, 1)])
return out[0]
}
@(private = "file")
load_fn :: proc(t: ^testing.T, doc: ^Document, src: string) -> ^Function {
f, err := function_load(doc, cf_parse(doc, src))
testing.expect_value(t, err, Error.None)
return f
}
@(private = "file")
load_sampled :: proc(t: ^testing.T, doc: ^Document, dict_src: string, payload: []byte) -> ^Function {
f, err := function_load(doc, cf_stream(doc, dict_src, payload))
testing.expect_value(t, err, Error.None)
return f
}
@(private = "file")
load_ps :: proc(t: ^testing.T, doc: ^Document, program: string) -> ^Function {
return load_sampled(t, doc,
"<< /FunctionType 4 /Domain [0 1] /Range [-100000 100000] >>", cf_bytes(program))
}
// ---------------------------------------------------------------------------
// Type 2: exponential interpolation
// ---------------------------------------------------------------------------
@(test)
test_function_type2_defaults :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// C0 defaults to [0.0] and C1 to [1.0], so this is the identity.
f := load_fn(t, doc, "<< /FunctionType 2 /Domain [0 1] /N 1 >>")
if f == nil { return }
testing.expect_value(t, f.n_in, 1)
testing.expect_value(t, f.n_out, 1)
expect_near(t, eval1(f, 0), 0, EPS, "f(0)")
expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25)")
expect_near(t, eval1(f, 1), 1, EPS, "f(1)")
}
@(test)
test_function_type2_exponent_and_outputs :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// C0 + x^N (C1 - C0) with N = 2: at x = 0.5 the factor is 0.25.
f := load_fn(t, doc,
"<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0] /C1 [1 0.5 0] /N 2 >>")
if f == nil { return }
testing.expect_value(t, f.n_out, 3)
in_ := [1]f64{0.5}
out: [3]f64
function_eval(f, in_[:], out[:])
expect_near(t, out[0], 0.25, EPS, "out0")
expect_near(t, out[1], 0.125, EPS, "out1")
expect_near(t, out[2], 0, EPS, "out2")
// C0 is returned exactly at the bottom of the domain whatever N is.
in_[0] = 0
function_eval(f, in_[:], out[:])
expect_near(t, out[0], 0, EPS, "out0 at 0")
}
// A negative base under a fractional exponent has no real value. The domain is
// supposed to exclude it; a file whose domain does not must still produce a
// number rather than a NaN.
@(test)
test_function_type2_negative_base :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := load_fn(t, doc,
"<< /FunctionType 2 /Domain [-1 1] /C0 [0] /C1 [1] /N 0.5 >>")
if f == nil { return }
v := eval1(f, -1)
testing.expect(t, is_finite(v), "a negative base must not produce a NaN")
}
// ---------------------------------------------------------------------------
// Domain and range clamping (ISO 32000-2, 7.10.1)
// ---------------------------------------------------------------------------
@(test)
test_function_domain_clamped :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := load_fn(t, doc, "<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [1] /N 1 >>")
if f == nil { return }
// Inputs outside /Domain are clamped to it, not extrapolated.
expect_near(t, eval1(f, 5), 1, EPS, "f(5)")
expect_near(t, eval1(f, -5), 0, EPS, "f(-5)")
// And a non-finite input cannot leak through into the output.
testing.expect(t, is_finite(eval1(f, math.INF_F64)), "infinite input")
testing.expect(t, is_finite(eval1(f, math.QNAN_F64)), "NaN input")
}
@(test)
test_function_range_clamped :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// The interpolation reaches 1.0 but /Range stops at 0.5.
f := load_fn(t, doc,
"<< /FunctionType 2 /Domain [0 1] /Range [0 0.5] /C0 [0] /C1 [1] /N 1 >>")
if f == nil { return }
expect_near(t, eval1(f, 1), 0.5, EPS, "f(1) clamped to /Range")
expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25) inside /Range")
// A type 4 program is clamped the same way: without it a shading paints
// whatever the calculator happened to leave behind.
g := load_sampled(t, doc,
"<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", cf_bytes("{ pop 42 }"))
if g == nil { return }
expect_near(t, eval1(g, 0), 1, EPS, "42 clamped to /Range")
}
// ---------------------------------------------------------------------------
// Type 0: sampled
// ---------------------------------------------------------------------------
@(test)
test_function_type0_linear :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Two 8-bit samples, 0 and 255, over [0 1].
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF})
if f == nil { return }
testing.expect_value(t, f.n_in, 1)
testing.expect_value(t, f.n_out, 1)
expect_near(t, eval1(f, 0), 0, EPS, "f(0)")
expect_near(t, eval1(f, 1), 1, EPS, "f(1)")
// Halfway between the two samples is 127.5/255.
expect_near(t, eval1(f, 0.5), 0.5, EPS, "f(0.5)")
expect_near(t, eval1(f, 0.25), 0.25, EPS, "f(0.25)")
}
@(test)
test_function_type0_two_outputs :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Two samples of two outputs each, interleaved: (0,255) then (255,0).
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1 0 1] /Size [2] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF, 0xFF, 0x00})
if f == nil { return }
testing.expect_value(t, f.n_out, 2)
in_ := [1]f64{0.5}
out: [2]f64
function_eval(f, in_[:], out[:])
expect_near(t, out[0], 0.5, EPS, "out0 midway")
expect_near(t, out[1], 0.5, EPS, "out1 midway")
in_[0] = 1
function_eval(f, in_[:], out[:])
expect_near(t, out[0], 1, EPS, "out0 at 1")
expect_near(t, out[1], 0, EPS, "out1 at 1")
}
// A 2x2 table: the first input varies fastest (7.10.2), so the samples are
// (0,0) (1,0) (0,1) (1,1).
@(test)
test_function_type0_bilinear :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1 0 1] /Range [0 1] /Size [2 2] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF, 0xFF, 0x00})
if f == nil { return }
testing.expect_value(t, f.n_in, 2)
check :: proc(t: ^testing.T, f: ^Function, x, y, want: f64, what: string) {
in_ := [2]f64{x, y}
out: [1]f64
function_eval(f, in_[:], out[:])
if !(abs(out[0] - want) <= 1e-9) {
testing.expectf(t, false, "%s: got %f, want %f", what, out[0], want)
}
}
check(t, f, 0, 0, 0, "corner (0,0)")
check(t, f, 1, 0, 1, "corner (1,0)")
check(t, f, 0, 1, 1, "corner (0,1)")
check(t, f, 1, 1, 0, "corner (1,1)")
// The centre averages all four corners, which is what makes this a test of
// interpolation rather than of nearest-neighbour lookup.
check(t, f, 0.5, 0.5, 0.5, "centre")
check(t, f, 0.5, 0, 0.5, "midway along x")
check(t, f, 0.25, 0, 0.25, "quarter along x")
check(t, f, 0, 0.75, 0.75, "three quarters along y")
}
// Three inputs, eight samples: sample value 0 or 255 by parity, so every
// corner and the centre have hand-checkable values.
@(test)
test_function_type0_three_inputs :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Index = i0 + 2*i1 + 4*i2. Values: 0, 255, 0, 255, 0, 255, 0, 255 -- so
// the table depends only on the first input.
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1 0 1 0 1] /Range [0 1] /Size [2 2 2] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF})
if f == nil { return }
testing.expect_value(t, f.n_in, 3)
check :: proc(t: ^testing.T, f: ^Function, x, y, z, want: f64, what: string) {
in_ := [3]f64{x, y, z}
out: [1]f64
function_eval(f, in_[:], out[:])
if !(abs(out[0] - want) <= 1e-9) {
testing.expectf(t, false, "%s: got %f, want %f", what, out[0], want)
}
}
check(t, f, 0, 0, 0, 0, "origin")
check(t, f, 1, 0, 0, 1, "along x")
check(t, f, 0, 1, 1, 0, "y and z do not matter")
check(t, f, 0.5, 0.5, 0.5, 0.5, "centre")
check(t, f, 0.25, 1, 0.5, 0.25, "quarter along x, anywhere else")
}
// Every bit width the format allows (Table 38). One sample of all zero bits
// and one of all one bits, so the decoded values must be exactly 0 and 1
// whatever the width is.
@(test)
test_function_type0_bit_widths :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
Case :: struct {
dict: string,
payload: []byte,
}
cases := []Case{
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 1 >>",
[]byte{0b0100_0000}},
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 2 >>",
[]byte{0b0011_0000}},
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 4 >>",
[]byte{0x0F}},
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF}},
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 12 >>",
[]byte{0x00, 0x0F, 0xFF}},
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 16 >>",
[]byte{0x00, 0x00, 0xFF, 0xFF}},
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 24 >>",
[]byte{0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF}},
{"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 32 >>",
[]byte{0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}},
}
for c, i in cases {
f := load_sampled(t, doc, c.dict, c.payload)
if f == nil {
testing.expectf(t, false, "case %d did not load", i)
continue
}
expect_near(t, eval1(f, 0), 0, EPS, "low sample")
expect_near(t, eval1(f, 1), 1, EPS, "high sample")
expect_near(t, eval1(f, 0.5), 0.5, EPS, "midpoint")
}
// A width the format does not define is refused rather than guessed at.
_, err := function_load(doc, cf_stream(doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 7 >>",
[]byte{0x00, 0xFF}))
testing.expect_value(t, err, Error.Bad_Function)
}
@(test)
test_function_type0_encode_and_decode :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// /Encode reverses the table: input 0 reads the last sample.
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 " +
"/Encode [1 0] >>",
[]byte{0x00, 0xFF})
if f == nil { return }
expect_near(t, eval1(f, 0), 1, EPS, "reversed f(0)")
expect_near(t, eval1(f, 1), 0, EPS, "reversed f(1)")
// /Decode maps the raw samples onto a different interval than /Range does.
g := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 100] /Size [2] /BitsPerSample 8 " +
"/Decode [10 20] >>",
[]byte{0x00, 0xFF})
if g == nil { return }
expect_near(t, eval1(g, 0), 10, EPS, "decoded low")
expect_near(t, eval1(g, 1), 20, EPS, "decoded high")
expect_near(t, eval1(g, 0.5), 15, EPS, "decoded midpoint")
// A domain that is not [0 1] is mapped onto the samples by /Encode's
// default, which is [0 Size-1].
h := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [100 200] /Range [0 1] /Size [2] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF})
if h == nil { return }
expect_near(t, eval1(h, 100), 0, EPS, "domain low")
expect_near(t, eval1(h, 150), 0.5, EPS, "domain middle")
expect_near(t, eval1(h, 200), 1, EPS, "domain high")
}
// /Order 3 asks for a cubic spline; linear interpolation is substituted, which
// the format allows. The function must still load and still interpolate.
@(test)
test_function_type0_cubic_falls_back :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2] /BitsPerSample 8 /Order 3 >>",
[]byte{0x00, 0xFF})
if f == nil { return }
testing.expect_value(t, f.order, 3)
expect_near(t, eval1(f, 0.5), 0.5, EPS, "cubic falls back to linear")
}
// A sample table shorter than /Size claims is common in truncated files. The
// missing samples read as zero; nothing reads past the buffer.
@(test)
test_function_type0_truncated_samples :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [16] /BitsPerSample 8 >>",
[]byte{0xFF, 0xFF})
if f == nil { return }
expect_near(t, eval1(f, 0), 1, EPS, "the samples that are there")
expect_near(t, eval1(f, 1), 0, EPS, "the ones that are not read as zero")
testing.expect(t, doc_has_repair(doc, .Bad_Resource), "a short table is a repair")
}
@(test)
test_function_type0_bad_sizes :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// A zero dimension would make every index into the table invalid.
f := load_sampled(t, doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [0] /BitsPerSample 8 >>",
[]byte{0x80})
if f != nil {
testing.expect(t, is_finite(eval1(f, 0.5)), "a zero /Size must still evaluate")
}
// A table with more samples than the library will hold is refused, not
// allocated.
_, err := function_load(doc, cf_stream(doc,
"<< /FunctionType 0 /Domain [0 1] /Range [0 1] /Size [2000000000] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF}))
testing.expect_value(t, err, Error.Bad_Function)
// /Range is what says how many outputs a sample carries; without it the
// table cannot be read at all.
_, err2 := function_load(doc, cf_stream(doc,
"<< /FunctionType 0 /Domain [0 1] /Size [2] /BitsPerSample 8 >>",
[]byte{0x00, 0xFF}))
testing.expect_value(t, err2, Error.Bad_Function)
}
// ---------------------------------------------------------------------------
// Type 3: stitching
// ---------------------------------------------------------------------------
@(test)
test_function_type3_boundary :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Two sub-functions that disagree at the bound: the first ends at 0.5, the
// second starts at 1.0. An input exactly on the bound belongs to the later
// one (7.10.4), so 0.5 must read 1.0 and not 0.5.
f := load_fn(t, doc,
"<< /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [" +
"<< /FunctionType 2 /Domain [0 1] /C0 [0] /C1 [0.5] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [1] /C1 [0] /N 1 >>] >>")
if f == nil { return }
testing.expect_value(t, f.n_in, 1)
testing.expect_value(t, f.n_out, 1)
// First half: [0 0.5] encodes onto [0 1], so 0.25 lands at 0.5 of a
// function that runs 0 -> 0.5.
expect_near(t, eval1(f, 0), 0, EPS, "start of the first sub-function")
expect_near(t, eval1(f, 0.25), 0.25, EPS, "middle of the first sub-function")
expect_near(t, eval1(f, 0.5), 1, EPS, "exactly on the bound is the later function")
expect_near(t, eval1(f, 0.75), 0.5, EPS, "middle of the second sub-function")
expect_near(t, eval1(f, 1), 0, EPS, "end of the domain")
}
@(test)
test_function_type3_three_way :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Three constant sub-functions, so the value alone says which one ran.
f := load_fn(t, doc,
"<< /FunctionType 3 /Domain [0 1] /Bounds [0.25 0.75] /Encode [0 1 0 1 0 1] /Functions [" +
"<< /FunctionType 2 /Domain [0 1] /C0 [10] /C1 [10] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [20] /C1 [20] /N 1 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [30] /C1 [30] /N 1 >>] >>")
if f == nil { return }
expect_near(t, eval1(f, 0), 10, EPS, "below the first bound")
expect_near(t, eval1(f, 0.24), 10, EPS, "just below the first bound")
expect_near(t, eval1(f, 0.25), 20, EPS, "on the first bound")
expect_near(t, eval1(f, 0.74), 20, EPS, "just below the second bound")
expect_near(t, eval1(f, 0.75), 30, EPS, "on the second bound")
expect_near(t, eval1(f, 1), 30, EPS, "top of the domain")
}
// A stitching function that lists itself among its sub-functions. Loading must
// terminate, and the branch that is not self-referential must still work.
@(test)
test_function_type3_self_reference :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f, err := function_load(doc, Ref{7, 0})
testing.expect_value(t, err, Error.None)
if f == nil { return }
// The second sub-function is the ordinary one: [0.5 1] encodes onto [0 1],
// so 0.75 lands halfway up a ramp from 0 to 1.
expect_near(t, eval1(f, 0.75), 0.5, EPS, "the sub-function that is not a cycle")
// And the self-referential branch terminates with a defined value.
testing.expect(t, is_finite(eval1(f, 0.25)), "the cyclic branch must terminate")
testing.expect(t, doc_has_repair(doc, .Recursion_Limit_Hit), "the cycle is a repair")
}
@(test)
test_function_type3_broken_sub_function :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// One sub-function is nonsense. The other must still be reachable.
f := load_fn(t, doc,
"<< /FunctionType 3 /Domain [0 1] /Bounds [0.5] /Encode [0 1 0 1] /Functions [" +
"<< /FunctionType 9 >> " +
"<< /FunctionType 2 /Domain [0 1] /C0 [7] /C1 [7] /N 1 >>] >>")
if f == nil { return }
expect_near(t, eval1(f, 0.75), 7, EPS, "the sub-function that loaded")
expect_near(t, eval1(f, 0.25), 0, EPS, "the one that did not reads as zero")
}
// ---------------------------------------------------------------------------
// Type 4: the PostScript calculator
// ---------------------------------------------------------------------------
@(test)
test_function_type4_operators :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
Case :: struct {
program: string,
want: f64,
}
// Every case is evaluated at x = 0, and the input stays on the stack below
// the result, so the value asserted is the top of the stack.
cases := []Case{
// Arithmetic.
{"{ 2 3 add }", 5},
{"{ 5 3 sub }", 2},
{"{ 3 4 mul }", 12},
{"{ 7 2 div }", 3.5},
{"{ 7 2 idiv }", 3},
{"{ -7 2 idiv }", -3},
{"{ 7 2 mod }", 1},
{"{ 5 neg }", -5},
{"{ -5 abs }", 5},
{"{ 1.2 ceiling }", 2},
{"{ -1.2 ceiling }", -1},
{"{ 1.8 floor }", 1},
{"{ -1.8 floor }", -2},
{"{ 2.5 round }", 3},
// PostScript rounds a halfway case towards positive infinity.
{"{ -2.5 round }", -2},
{"{ 1.9 truncate }", 1},
{"{ -1.9 truncate }", -1},
{"{ 9 sqrt }", 3},
{"{ 90 sin }", 1},
{"{ 0 sin }", 0},
{"{ 180 cos }", -1},
{"{ 0 cos }", 1},
// atan takes num then den and answers in degrees, never negative.
{"{ 1 0 atan }", 90},
{"{ 0 -1 atan }", 180},
{"{ -1 0 atan }", 270},
{"{ 0 1 atan }", 0},
{"{ 2 10 exp }", 1024},
{"{ 9 0.5 exp }", 3},
{"{ 1 ln }", 0},
{"{ 100 log }", 2},
{"{ 3.9 cvi }", 3},
{"{ -3.9 cvi }", -3},
{"{ 3 cvr }", 3},
// Relational and boolean. Each result is turned back into a number by
// an ifelse so that one assertion covers both.
{"{ 1 1 eq { 10 } { 20 } ifelse }", 10},
{"{ 1 2 eq { 10 } { 20 } ifelse }", 20},
{"{ 1 2 ne { 10 } { 20 } ifelse }", 10},
{"{ 2 1 gt { 10 } { 20 } ifelse }", 10},
{"{ 1 1 gt { 10 } { 20 } ifelse }", 20},
{"{ 2 2 ge { 10 } { 20 } ifelse }", 10},
{"{ 1 2 lt { 10 } { 20 } ifelse }", 10},
{"{ 2 2 le { 10 } { 20 } ifelse }", 10},
{"{ true false and { 10 } { 20 } ifelse }", 20},
{"{ true true and { 10 } { 20 } ifelse }", 10},
{"{ true false or { 10 } { 20 } ifelse }", 10},
{"{ true true xor { 10 } { 20 } ifelse }", 20},
{"{ true false xor { 10 } { 20 } ifelse }", 10},
{"{ false not { 10 } { 20 } ifelse }", 10},
{"{ true not { 10 } { 20 } ifelse }", 20},
// On integers the same four operators are bitwise, not logical.
{"{ 12 10 and }", 8},
{"{ 12 10 or }", 14},
{"{ 12 10 xor }", 6},
{"{ 0 not }", -1},
{"{ 1 3 bitshift }", 8},
{"{ 16 -3 bitshift }", 2},
// Stack operators.
{"{ 1 2 pop }", 1},
{"{ 1 2 exch }", 1},
{"{ 7 dup add }", 14},
{"{ 5 7 2 copy add }", 12},
{"{ 1 2 3 2 index }", 1},
{"{ 1 2 3 3 1 roll }", 2},
{"{ 1 2 3 3 -1 roll }", 1},
{"{ 1 2 3 3 0 roll }", 3},
// Conditionals.
{"{ true { 10 } if }", 10},
// Nothing is pushed, so the top of the stack is still the input, 0.
{"{ false { 10 } if }", 0},
{"{ true { 1 2 add } { 99 } ifelse }", 3},
{"{ false { 99 } { 1 2 add } ifelse }", 3},
// Nested conditionals, and a branch that is several operators long.
{"{ true { false { 1 } { 2 } ifelse } { 3 } ifelse }", 2},
{"{ 3 4 lt { 3 4 add 2 mul } { 0 } ifelse }", 14},
// The input itself is on the stack when the program starts.
{"{ 1 add }", 1},
// A comment runs to the end of the line.
{"{ 2 3 add % this is ignored\n }", 5},
// A word this calculator has no operator for is stepped over rather
// than abandoning the program.
{"{ 2 3 add nosuchop }", 5},
}
for c, i in cases {
f := load_ps(t, doc, c.program)
if f == nil {
testing.expectf(t, false, "case %d (%s) did not load", i, c.program)
continue
}
got := eval1(f, 0)
if !(abs(got - c.want) <= 1e-9) {
testing.expectf(t, false, "%s: got %f, want %f", c.program, got, c.want)
}
}
}
// Division by zero, the square root of a negative, the logarithm of zero and
// an empty stack must all produce a defined value rather than a NaN, an
// infinity or a crash.
@(test)
test_function_type4_defined_on_bad_input :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
Case :: struct {
program: string,
want: f64,
}
cases := []Case{
{"{ 1 0 div }", 0},
{"{ 1 0 idiv }", 0},
{"{ 1 0 mod }", 0},
{"{ -4 sqrt }", 0},
{"{ 0 ln }", 0},
{"{ -1 ln }", 0},
{"{ 0 log }", 0},
// Every operand gone, then an operator that wants two.
{"{ pop add }", 0},
{"{ pop pop pop sub }", 0},
{"{ pop dup }", 0},
{"{ pop exch }", 0},
// exp overflowing to infinity must not leave an infinity behind.
{"{ 1000 1000 exp }", 0},
// index past the bottom of the stack.
{"{ 99 index }", 0},
// roll and copy with counts that make no sense.
{"{ 1 2 -3 1 roll }", 2},
{"{ 1 2 99 1 roll }", 2},
{"{ 5 -1 copy }", 5},
// A block with no `if` after it never runs.
{"{ 5 { 99 } }", 5},
// An `if` with no block before it is skipped.
{"{ 5 if }", 5},
}
for c in cases {
f := load_ps(t, doc, c.program)
if f == nil {
testing.expectf(t, false, "%s did not load", c.program)
continue
}
got := eval1(f, 0)
testing.expectf(t, is_finite(got), "%s produced a non-finite value", c.program)
if !(abs(got - c.want) <= 1e-9) {
testing.expectf(t, false, "%s: got %f, want %f", c.program, got, c.want)
}
}
}
// A program that pushes more than the hundred operands the format allows
// (7.10.5) stops cleanly and still produces a value.
@(test)
test_function_type4_stack_overflow :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// 200 pushes, in arena memory so that the stream can keep pointing at it.
src := make([]byte, 2 + 2 * 200 + 1, doc.allocator)
src[0], src[1] = '{', ' '
for i in 0 ..< 200 {
src[2 + 2 * i] = '1'
src[3 + 2 * i] = ' '
}
src[len(src) - 1] = '}'
f := load_sampled(t, doc, "<< /FunctionType 4 /Domain [0 1] /Range [0 10] >>", src)
if f == nil { return }
got := eval1(f, 0)
testing.expect(t, is_finite(got), "an overflowing program still produces a value")
expect_near(t, got, 1, EPS, "the last value pushed before the stack filled")
}
// A program that jumps backwards forever is not something a compiler here can
// produce, but a Function is a plain struct and the interpreter has to survive
// one anyway. The step budget is what ends it.
@(test)
test_function_type4_infinite_loop_terminates :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := new(Function, doc.allocator)
f.kind = .PostScript
f.n_in = 1
f.n_out = 1
f.domain = fn_make_pair(doc, 0, 1)
f.range = fn_make_pair(doc, 0, 1)
prog := make([]Ps_Op, 1, doc.allocator)
prog[0] = Ps_Op{kind = .Jump, target = 0}
f.program = prog
// Reaching the assertion at all is the test: without a step budget this
// never returns.
expect_near(t, eval1(f, 0.25), 0.25, EPS, "the input is still on the stack")
}
@(test)
test_function_type4_deep_nesting :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Braces nested past the compiler's depth limit are refused rather than
// recursing the native stack away.
n := MAX_PS_DEPTH + 8
src := make([]byte, 1 + 2 * n, doc.allocator)
src[0] = '{'
for i in 0 ..< n {
src[1 + i] = '{'
}
for i in 0 ..< n {
src[1 + n + i] = '}'
}
_, err := function_load(doc, cf_stream(doc,
"<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", src))
testing.expect_value(t, err, Error.Bad_Function)
}
@(test)
test_function_type4_needs_range :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
// Without /Range there is no way to know how many of the values left on
// the stack are outputs.
_, err := function_load(doc, cf_stream(doc,
"<< /FunctionType 4 /Domain [0 1] >>", cf_bytes("{ 1 }")))
testing.expect_value(t, err, Error.Bad_Function)
// An empty program is not a function either.
_, err2 := function_load(doc, cf_stream(doc,
"<< /FunctionType 4 /Domain [0 1] /Range [0 1] >>", cf_bytes("")))
testing.expect_value(t, err2, Error.Bad_Function)
}
// ---------------------------------------------------------------------------
// Loading: caching, and what a broken function does
// ---------------------------------------------------------------------------
@(test)
test_function_cached_by_object :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
a, err_a := function_load(doc, Ref{6, 0})
b, err_b := function_load(doc, Ref{6, 0})
testing.expect_value(t, err_a, Error.None)
testing.expect_value(t, err_b, Error.None)
testing.expect(t, a != nil && a == b, "the same object must build one function")
// A different object is a different function.
c, _ := function_load(doc, Ref{8, 0})
testing.expect(t, c != nil && c != a, "different objects are different functions")
}
@(test)
test_function_rejects_nonsense :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
_, e1 := function_load(doc, nil)
testing.expect_value(t, e1, Error.Bad_Function)
_, e2 := function_load(doc, cf_parse(doc, "42"))
testing.expect_value(t, e2, Error.Bad_Function)
// An unknown /FunctionType.
_, e3 := function_load(doc, cf_parse(doc, "<< /FunctionType 5 /Domain [0 1] >>"))
testing.expect_value(t, e3, Error.Bad_Function)
// A type 2 that is missing everything still loads, because C0 and C1 have
// defaults and /Domain does not need to be trusted.
f, e4 := function_load(doc, cf_parse(doc, "<< /FunctionType 2 >>"))
testing.expect_value(t, e4, Error.None)
if f != nil {
expect_near(t, eval1(f, 0.5), 0.5, EPS, "defaulted type 2")
}
// Evaluating nothing writes zeros rather than leaving the caller's buffer
// untouched.
out := [3]f64{9, 9, 9}
in_ := [1]f64{0.5}
function_eval(nil, in_[:], out[:])
testing.expect_value(t, out[0], 0)
testing.expect_value(t, out[1], 0)
testing.expect_value(t, out[2], 0)
}
// A caller may ask for fewer outputs than the function produces, and must not
// have anything written past the end of its buffer.
@(test)
test_function_short_output_buffer :: proc(t: ^testing.T) {
doc := cf_open(t)
defer cf_close(t, doc)
f := load_fn(t, doc,
"<< /FunctionType 2 /Domain [0 1] /C0 [0 0 0 0] /C1 [1 1 1 1] /N 1 >>")
if f == nil { return }
testing.expect_value(t, f.n_out, 4)
guard := [3]f64{0, 0, 77}
in_ := [1]f64{1}
function_eval(f, in_[:], guard[:2])
expect_near(t, guard[0], 1, EPS, "first output")
expect_near(t, guard[1], 1, EPS, "second output")
testing.expect_value(t, guard[2], 77)
}