Compare commits

..

82 Commits

Author SHA1 Message Date
9407162543 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation-backup 2024-12-09 14:04:18 -05:00
067b83b468 Manual resolution of snapshot conflicts 2024-12-09 13:56:33 -05:00
c5b30341eb Add unit tests for doesSceneHaveExtrudedSketch 2024-12-09 12:26:21 -05:00
3e6441b563 Fix test annotations 2024-12-09 11:36:46 -05:00
acafcf2d4d Apply suggestions from Frank's review
Co-authored-by: Frank Noirot <frank@zoo.dev>
2024-12-09 11:33:41 -05:00
b9f31d94d5 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-05 18:43:44 -05:00
9e03b58ae5 Cap and wall pw test 2024-12-05 18:21:20 -05:00
c591f73c70 Working multi-face shell across types 2024-12-05 17:56:18 -05:00
9330aaba13 Trigger CI 2024-12-05 17:27:16 -05:00
5a14f0189e Look at this (photo)Graph *in the voice of Nickelback* 2024-12-05 22:14:20 +00:00
9af001f22e Lint 2024-12-05 17:04:02 -05:00
435d1ea52e WIP circular dep 2024-12-05 16:53:57 -05:00
54847139f2 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-05 15:00:34 -05:00
9369a17ea7 WIP mutliple faces 2024-12-05 14:59:44 -05:00
9ee2e7c3b0 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-05 19:42:39 +00:00
28ae261e5f Lint fix 2024-12-05 14:38:23 -05:00
e4b0de0ead Add selection guard and clean up 2024-12-05 14:06:09 -05:00
12859598a3 Merge branch 'main' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-05 13:02:35 -05:00
e9a334f433 Fix lint 2024-12-05 13:02:22 -05:00
e470a7b4af Add shell wall test 2024-12-05 12:33:46 -05:00
602c39f63c Add pw tests for cap shell 2024-12-05 12:03:45 -05:00
4994aa6f61 Handle walls 2024-12-05 11:38:03 -05:00
4aa07b81db Add extrude lookup for more generic shell 2024-12-05 10:26:58 -05:00
89ef4b3243 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-05 14:55:04 +00:00
001c9a8219 Update from main 2024-12-05 09:49:14 -05:00
4dde3f60e0 WIP: first time working shell mod 2024-12-04 18:41:45 -05:00
f407c53032 WIP: closer 2024-12-04 17:53:04 -05:00
9ba584487a WIP: more additions 2024-12-04 17:01:11 -05:00
96b66d6bca Merge branch 'pierremtb/issue4470-loft-ui' into pierremtb/issue4472-Add-shell-point-and-click-operation 2024-12-04 16:44:40 -05:00
8d66f3ffad Rollback pw values to pre cam change 2024-12-04 16:43:29 -05:00
f4c54cbbe4 WIP: initial shell code addition 2024-12-04 16:38:44 -05:00
5156b847f3 Trigger CI 2024-12-04 15:56:48 -05:00
ded9f2c56b A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 20:54:59 +00:00
5db5f79f9a Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-12-04 15:46:18 -05:00
6a883f4a8d A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 20:43:39 +00:00
07b91f0fb1 Trigger CI 2024-12-04 15:39:26 -05:00
1b2e213afe A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 20:37:28 +00:00
f48a23c35e A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 20:36:20 +00:00
7e1d102496 Revert snapshots 2024-12-04 15:32:14 -05:00
94cb2535c0 Fix typo 2024-12-04 15:21:03 -05:00
9e08ec9096 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 20:00:36 +00:00
17bd8ec32a A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 19:58:28 +00:00
d0f12e85e5 Trigger CI 2024-12-04 14:54:05 -05:00
94d185944e A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 19:52:22 +00:00
c38b2270c3 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 19:51:34 +00:00
967ad66c98 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 19:47:04 +00:00
afeca9ca39 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 19:46:25 +00:00
61242282f0 Trigger CI 2024-12-04 14:42:00 -05:00
0065df13ce A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 16:55:39 +00:00
01c8d45c13 Remove comments 2024-12-04 11:44:49 -05:00
8b25527f21 Move error logic out of loftSketches, fix pw tests 2024-12-04 11:39:35 -05:00
2abd980de9 Move to fromPromise-based Actor 2024-12-04 11:02:51 -05:00
f783deb706 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 15:25:38 +00:00
f4dd295ca1 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 15:17:19 +00:00
ceaa85fe3f A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 15:16:16 +00:00
3991bd9173 Trigger CI 2024-12-04 10:11:47 -05:00
b8f9da36c0 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 10:54:14 +00:00
283315b5d2 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-04 10:44:01 +00:00
e204dfe564 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-12-04 10:43:15 +00:00
208a36196b Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-12-04 05:38:43 -05:00
660a349588 Add pw test for preselected sketches 2024-12-03 15:17:21 -05:00
56c37da317 Clean up loftSketches function 2024-12-03 15:02:56 -05:00
a46734b76d Add test for doesSceneHaveSweepableSketch with count = 2 2024-12-03 14:39:06 -05:00
4347e0cf84 Clean up and working pw test 2024-12-03 13:49:26 -05:00
df3e541cdf A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-12-03 17:51:57 +00:00
b1cec443b9 Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-12-03 12:48:32 -05:00
967f49055d Lint 2024-12-02 14:30:43 -05:00
fe977524b5 First point-click loft test (not working locally, loft gets inserted at the wrong place) 2024-12-02 14:19:32 -05:00
d6f271fb0f Enable multiple selections after the button click 2024-12-01 07:00:13 -05:00
e851b2bcc4 Merge branch 'main' into pierremtb/issue4470-loft-ui 2024-11-29 20:01:32 -05:00
be569c91de Clean up 2024-11-29 19:51:56 -05:00
5080e304b9 Appends the loft line after the 'last' sketch in the code 2024-11-29 18:30:15 -05:00
f4e75b7b4f More checks 2024-11-29 13:38:52 -05:00
31cbc90f56 WIP selections 2024-11-29 12:37:53 -05:00
a7d3552472 WIP selections 2024-11-29 11:17:31 -05:00
e984b20664 First pass at handling more than 2 sketches 2024-11-29 10:09:10 -05:00
0c2cd24bda Working loft for two sketches in the right hardcoded order 2024-11-28 20:28:37 -05:00
9b2de237b8 Add selection guard 2024-11-28 16:45:41 -05:00
c79c02f18e A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-11-28 20:25:44 +00:00
4851aa2d71 A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest-8-cores) 2024-11-28 20:17:17 +00:00
76fafa6fd0 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest-8-cores) 2024-11-28 20:11:35 +00:00
647ca11e08 WIP: experimenting with Loft UI
Relates to #4470
2024-11-27 15:55:01 -05:00
83 changed files with 2457 additions and 6221 deletions

View File

@ -45,7 +45,7 @@ circles = map([1..3], drawCircle)
```js ```js
r = 10 // radius r = 10 // radius
// Call `map`, using an anonymous function instead of a named one. // Call `map`, using an anonymous function instead of a named one.
circles = map([1..3], fn(id) { circles = map([1..3], (id) {
return startSketchOn("XY") return startSketchOn("XY")
|> circle({ center = [id * 2 * r, 0], radius = r }, %) |> circle({ center = [id * 2 * r, 0], radius = r }, %)
}) })

View File

@ -61,7 +61,7 @@ assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
// an anonymous `add` function as its parameter, instead of declaring a // an anonymous `add` function as its parameter, instead of declaring a
// named function outside. // named function outside.
arr = [1, 2, 3] arr = [1, 2, 3]
sum = reduce(arr, 0, fn(i, result_so_far) { sum = reduce(arr, 0, (i, result_so_far) {
return i + result_so_far return i + result_so_far
}) })
@ -84,7 +84,7 @@ fn decagon(radius) {
// Use a `reduce` to draw the remaining decagon sides. // Use a `reduce` to draw the remaining decagon sides.
// For each number in the array 1..10, run the given function, // For each number in the array 1..10, run the given function,
// which takes a partially-sketched decagon and adds one more edge to it. // which takes a partially-sketched decagon and adds one more edge to it.
fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) { fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {
// Draw one edge of the decagon. // Draw one edge of the decagon.
x = cos(stepAngle * i) * radius x = cos(stepAngle * i) * radius
y = sin(stepAngle * i) * radius y = sin(stepAngle * i) * radius

View File

@ -100436,7 +100436,7 @@
"deprecated": false, "deprecated": false,
"examples": [ "examples": [
"r = 10 // radius\nfn drawCircle(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n}\n\n// Call `drawCircle`, passing in each element of the array.\n// The outputs from each `drawCircle` form a new array,\n// which is the return value from `map`.\ncircles = map([1..3], drawCircle)", "r = 10 // radius\nfn drawCircle(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n}\n\n// Call `drawCircle`, passing in each element of the array.\n// The outputs from each `drawCircle` form a new array,\n// which is the return value from `map`.\ncircles = map([1..3], drawCircle)",
"r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1..3], fn(id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n})" "r = 10 // radius\n// Call `map`, using an anonymous function instead of a named one.\ncircles = map([1..3], (id) {\n return startSketchOn(\"XY\")\n |> circle({ center = [id * 2 * r, 0], radius = r }, %)\n})"
] ]
}, },
{ {
@ -146129,8 +146129,8 @@
"deprecated": false, "deprecated": false,
"examples": [ "examples": [
"// This function adds two numbers.\nfn add(a, b) {\n return a + b\n}\n\n// This function adds an array of numbers.\n// It uses the `reduce` function, to call the `add` function on every\n// element of the `arr` parameter. The starting value is 0.\nfn sum(arr) {\n return reduce(arr, 0, add)\n}\n\n/* The above is basically like this pseudo-code:\nfn sum(arr):\n let sumSoFar = 0\n for i in arr:\n sumSoFar = add(sumSoFar, i)\n return sumSoFar */\n\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")", "// This function adds two numbers.\nfn add(a, b) {\n return a + b\n}\n\n// This function adds an array of numbers.\n// It uses the `reduce` function, to call the `add` function on every\n// element of the `arr` parameter. The starting value is 0.\nfn sum(arr) {\n return reduce(arr, 0, add)\n}\n\n/* The above is basically like this pseudo-code:\nfn sum(arr):\n let sumSoFar = 0\n for i in arr:\n sumSoFar = add(sumSoFar, i)\n return sumSoFar */\n\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum([1, 2, 3]), 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, fn(i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")", "// This example works just like the previous example above, but it uses\n// an anonymous `add` function as its parameter, instead of declaring a\n// named function outside.\narr = [1, 2, 3]\nsum = reduce(arr, 0, (i, result_so_far) {\n return i + result_so_far\n})\n\n// We use `assertEqual` to check that our `sum` function gives the\n// expected result. It's good to check your work!\nassertEqual(sum, 6, 0.00001, \"1 + 2 + 3 summed is 6\")",
"// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many degrees from the previous angle.\n stepAngle = 1 / 10 * tau()\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius])\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return lineTo([x, y], partialDecagon)\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n let stepAngle = (1/10) * tau()\n let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])\n\n // Here's the reduce part.\n let partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n let x = cos(stepAngle * i) * radius\n let y = sin(stepAngle * i) * radius\n partialDecagon = lineTo([x, y], partialDecagon)\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close(%)" "// Declare a function that sketches a decagon.\nfn decagon(radius) {\n // Each side of the decagon is turned this many degrees from the previous angle.\n stepAngle = 1 / 10 * tau()\n\n // Start the decagon sketch at this point.\n startOfDecagonSketch = startSketchAt([cos(0) * radius, sin(0) * radius])\n\n // Use a `reduce` to draw the remaining decagon sides.\n // For each number in the array 1..10, run the given function,\n // which takes a partially-sketched decagon and adds one more edge to it.\n fullDecagon = reduce([1..10], startOfDecagonSketch, (i, partialDecagon) {\n // Draw one edge of the decagon.\n x = cos(stepAngle * i) * radius\n y = sin(stepAngle * i) * radius\n return lineTo([x, y], partialDecagon)\n })\n\n return fullDecagon\n}\n\n/* The `decagon` above is basically like this pseudo-code:\nfn decagon(radius):\n let stepAngle = (1/10) * tau()\n let startOfDecagonSketch = startSketchAt([(cos(0)*radius), (sin(0) * radius)])\n\n // Here's the reduce part.\n let partialDecagon = startOfDecagonSketch\n for i in [1..10]:\n let x = cos(stepAngle * i) * radius\n let y = sin(stepAngle * i) * radius\n partialDecagon = lineTo([x, y], partialDecagon)\n fullDecagon = partialDecagon // it's now full\n return fullDecagon */\n\n\n// Use the `decagon` function declared above, to sketch a decagon with radius 5.\ndecagon(5.0)\n |> close(%)"
] ]
}, },
{ {

View File

@ -812,7 +812,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
commandName: 'Shell', commandName: 'Shell',
}) })
await clickOnCap() await clickOnCap()
await app.page.waitForTimeout(500)
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.progressCmdBar() await cmdBar.progressCmdBar()
await cmdBar.expectState({ await cmdBar.expectState({
@ -828,7 +827,6 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => {
} else { } else {
await test.step(`Preselect the cap`, async () => { await test.step(`Preselect the cap`, async () => {
await clickOnCap() await clickOnCap()
await app.page.waitForTimeout(500)
}) })
await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => {

View File

@ -26,17 +26,7 @@ test.describe('Testing constraints', () => {
}) })
const u = await getUtils(page) const u = await getUtils(page)
// constants and locators const PUR = 400 / 37.5 //pixeltoUnitRatio
const lengthValue = {
old: '20',
new: '25',
}
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart() await u.waitForAuthSkipAppStart()
@ -46,26 +36,26 @@ test.describe('Testing constraints', () => {
await u.closeDebugPanel() await u.closeDebugPanel()
// Click the line of code for line. // Click the line of code for line.
// TODO remove this and reinstate `await topHorzSegmentClick()` await page.getByText(`line([0, 20], %)`).click() // TODO remove this and reinstate // await topHorzSegmentClick()
await page.getByText(`line([0, ${lengthValue.old}], %)`).click()
await page.waitForTimeout(100) await page.waitForTimeout(100)
// enter sketch again // enter sketch again
await page.getByRole('button', { name: 'Edit Sketch' }).click() await page.getByRole('button', { name: 'Edit Sketch' }).click()
await page.waitForTimeout(500) // wait for animation await page.waitForTimeout(500) // wait for animation
const startXPx = 500
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.down('Shift')
await page.mouse.click(834, 244)
await page.keyboard.up('Shift')
await page await page
.getByRole('button', { name: 'dimension Length', exact: true }) .getByRole('button', { name: 'dimension Length', exact: true })
.click() .click()
await expect(cmdBarKclInput).toHaveText('20') await page.getByText('Add constraining value').click()
await cmdBarKclInput.fill(lengthValue.new)
await expect(
page.getByText(`Can't calculate`),
`Something went wrong with the KCL expression evaluation`
).not.toBeVisible()
await cmdBarSubmitButton.click()
await expect(page.locator('.cm-content')).toHaveText( await expect(page.locator('.cm-content')).toHaveText(
`length001 = ${lengthValue.new}sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)` `length001 = 20sketch001 = startSketchOn('XY') |> startProfileAt([-10, -10], %) |> line([20, 0], %) |> angledLine([90, length001], %) |> xLine(-20, %)`
) )
// Make sure we didn't pop out of sketch mode. // Make sure we didn't pop out of sketch mode.
@ -76,6 +66,7 @@ test.describe('Testing constraints', () => {
await page.waitForTimeout(500) // wait for animation await page.waitForTimeout(500) // wait for animation
// Exit sketch // Exit sketch
await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10)
await page.keyboard.press('Escape') await page.keyboard.press('Escape')
await expect( await expect(
page.getByRole('button', { name: 'Exit Sketch' }) page.getByRole('button', { name: 'Exit Sketch' })
@ -533,7 +524,7 @@ part002 = startSketchOn('XZ')
}) })
} }
}) })
test.describe('Test Angle constraint single selection', () => { test.describe('Test Angle/Length constraint single selection', () => {
const cases = [ const cases = [
{ {
testName: 'Angle - Add variable', testName: 'Angle - Add variable',
@ -547,6 +538,18 @@ part002 = startSketchOn('XZ')
constraint: 'angle', constraint: 'angle',
value: '83, 78.33', value: '83, 78.33',
}, },
{
testName: 'Length - Add variable',
addVariable: true,
constraint: 'length',
value: '83, length001',
},
{
testName: 'Length - No variable',
addVariable: false,
constraint: 'length',
value: '83, 78.33',
},
] as const ] as const
for (const { testName, addVariable, value, constraint } of cases) { for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => { test(`${testName}`, async ({ page }) => {
@ -605,90 +608,6 @@ part002 = startSketchOn('XZ')
}) })
} }
}) })
test.describe('Test Length constraint single selection', () => {
const cases = [
{
testName: 'Length - Add variable',
addVariable: true,
constraint: 'length',
value: '83, length001',
},
{
testName: 'Length - No variable',
addVariable: false,
constraint: 'length',
value: '83, 78.33',
},
] as const
for (const { testName, addVariable, value, constraint } of cases) {
test(`${testName}`, async ({ page }) => {
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarKclVariableNameInput =
page.getByPlaceholder('Variable name')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
await page.addInitScript(async () => {
localStorage.setItem(
'persistCode',
`yo = 5
part001 = startSketchOn('XZ')
|> startProfileAt([-7.54, -26.74], %)
|> line([74.36, 130.4], %)
|> line([78.92, -120.11], %)
|> line([9.16, 77.79], %)
|> line([51.19, 48.97], %)
part002 = startSketchOn('XZ')
|> startProfileAt([299.05, 231.45], %)
|> xLine(-425.34, %, $seg_what)
|> yLine(-264.06, %)
|> xLine(segLen(seg_what), %)
|> lineTo([profileStartX(%), profileStartY(%)], %)`
)
})
const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 })
await u.waitForAuthSkipAppStart()
await page.getByText('line([74.36, 130.4], %)').click()
await page.getByRole('button', { name: 'Edit Sketch' }).click()
const line3 = await u.getSegmentBodyCoords(
`[data-overlay-index="${2}"]`
)
await page.mouse.click(line3.x, line3.y)
await page
.getByRole('button', {
name: 'Length: open menu',
})
.click()
await page.getByTestId('dropdown-constraint-' + constraint).click()
if (!addVariable) {
await test.step(`Clear the variable input`, async () => {
await cmdBarKclVariableNameInput.clear()
await cmdBarKclVariableNameInput.press('Backspace')
})
}
await expect(cmdBarKclInput).toHaveText('78.33')
await cmdBarSubmitButton.click()
const changedCode = `|> angledLine([${value}], %)`
await expect(page.locator('.cm-content')).toContainText(changedCode)
// checking active assures the cursor is where it should be
await expect(page.locator('.cm-activeLine')).toHaveText(changedCode)
// checking the count of the overlays is a good proxy check that the client sketch scene is in a good state
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
})
}
})
test.describe('Many segments - no modal constraints', () => { test.describe('Many segments - no modal constraints', () => {
const cases = [ const cases = [
{ {
@ -949,15 +868,6 @@ part002 = startSketchOn('XZ')
|> line([3.13, -2.4], %)` |> line([3.13, -2.4], %)`
) )
}) })
// constants and locators
const cmdBarKclInput = page
.getByTestId('cmd-bar-arg-value')
.getByRole('textbox')
const cmdBarSubmitButton = page.getByRole('button', {
name: 'arrow right Continue',
})
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -1018,8 +928,8 @@ part002 = startSketchOn('XZ')
// await page.getByRole('button', { name: 'length', exact: true }).click() // await page.getByRole('button', { name: 'length', exact: true }).click()
await page.getByTestId('dropdown-constraint-length').click() await page.getByTestId('dropdown-constraint-length').click()
await cmdBarKclInput.fill('10') await page.getByLabel('length Value').fill('10')
await cmdBarSubmitButton.click() await page.getByRole('button', { name: 'Add constraining value' }).click()
activeLinesContent = await page.locator('.cm-activeLine').all() activeLinesContent = await page.locator('.cm-activeLine').all()
await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`) await expect(activeLinesContent[0]).toHaveText(`|> xLine(length001, %)`)

View File

@ -91,14 +91,7 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count() await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0) ).toBeGreaterThan(0)
await unconstrainedLocator.click() await unconstrainedLocator.click()
await expect( await page.getByText('Add variable').click()
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await expect(page.locator('.cm-content')).toContainText(expectFinal) await expect(page.locator('.cm-content')).toContainText(expectFinal)
} }
@ -158,14 +151,7 @@ test.describe('Testing segment overlays', () => {
await page.getByTestId('constraint-symbol-popover').count() await page.getByTestId('constraint-symbol-popover').count()
).toBeGreaterThan(0) ).toBeGreaterThan(0)
await unconstrainedLocator.click() await unconstrainedLocator.click()
await expect( await page.getByText('Add variable').click()
page.getByTestId('cmd-bar-arg-value').getByRole('textbox')
).toBeFocused()
await page
.getByRole('button', {
name: 'arrow right Continue',
})
.click()
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText(
expectAfterUnconstrained expectAfterUnconstrained
) )

View File

@ -200,10 +200,7 @@ function CoreDump() {
() => new CoreDumpManager(engineCommandManager, codeManager, token), () => new CoreDumpManager(engineCommandManager, codeManager, token),
[] []
) )
// TODO: revisit once progress is made on upstream issue useHotkeyWrapper(['mod + shift + .'], () => {
// https://github.com/JohannesKlauss/react-hotkeys-hook/issues/1064
// const hotkey = process.platform !== 'linux' ? 'mod + shift + .' : 'mod + shift + >'
useHotkeyWrapper(['mod + shift + .', 'mod + shift + >'], () => {
toast toast
.promise( .promise(
coreDump(coreDumpManager, true), coreDump(coreDumpManager, true),

View File

@ -505,8 +505,7 @@ const ConstraintSymbol = ({
constrainInfo: ConstrainInfo constrainInfo: ConstrainInfo
verticalPosition: 'top' | 'bottom' verticalPosition: 'top' | 'bottom'
}) => { }) => {
const { commandBarSend } = useCommandsContext() const { context, send } = useModelingContext()
const { context } = useModelingContext()
const varNameMap: { const varNameMap: {
[key in ConstrainInfo['type']]: { [key in ConstrainInfo['type']]: {
varName: string varName: string
@ -625,18 +624,11 @@ const ConstraintSymbol = ({
// disabled={implicitDesc} TODO why does this change styles that are hard to override? // disabled={implicitDesc} TODO why does this change styles that are hard to override?
onClick={toSync(async () => { onClick={toSync(async () => {
if (!isConstrained) { if (!isConstrained) {
commandBarSend({ send({
type: 'Find and select command', type: 'Convert to variable',
data: { data: {
name: 'Constrain with named value', pathToNode,
groupId: 'modeling', variableName: varName,
argDefaultValues: {
currentValue: {
pathToNode,
variableName: varName,
valueText: value,
},
},
}, },
}) })
} else if (isConstrained) { } else if (isConstrained) {

View File

@ -8,16 +8,11 @@ import { getSystemTheme } from 'lib/theme'
import { useCalculateKclExpression } from 'lib/useCalculateKclExpression' import { useCalculateKclExpression } from 'lib/useCalculateKclExpression'
import { roundOff } from 'lib/utils' import { roundOff } from 'lib/utils'
import { varMentions } from 'lib/varCompletionExtension' import { varMentions } from 'lib/varCompletionExtension'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook' import { useHotkeys } from 'react-hotkeys-hook'
import styles from './CommandBarKclInput.module.css' import styles from './CommandBarKclInput.module.css'
import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst' import { createIdentifier, createVariableDeclaration } from 'lang/modifyAst'
import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor' import { useCodeMirror } from 'components/ModelingSidebar/ModelingPanes/CodeEditor'
import { useSelector } from '@xstate/react'
const machineContextSelector = (snapshot?: {
context: Record<string, unknown>
}) => snapshot?.context
function CommandBarKclInput({ function CommandBarKclInput({
arg, arg,
@ -36,44 +31,12 @@ function CommandBarKclInput({
arg.name arg.name
] as KclCommandValue | undefined ] as KclCommandValue | undefined
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const argMachineContext = useSelector( const defaultValue = (arg.defaultValue as string) || ''
arg.machineActor,
machineContextSelector
)
const defaultValue = useMemo(
() =>
arg.defaultValue
? arg.defaultValue instanceof Function
? arg.defaultValue(commandBarState.context, argMachineContext)
: arg.defaultValue
: '',
[arg.defaultValue, commandBarState.context, argMachineContext]
)
const initialVariableName = useMemo(() => {
// Use the configured variable name if it exists
if (arg.variableName !== undefined) {
return arg.variableName instanceof Function
? arg.variableName(commandBarState.context, argMachineContext)
: arg.variableName
}
// or derive it from the previously set value or the argument name
return previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name
}, [
arg.variableName,
commandBarState.context,
argMachineContext,
arg.name,
previouslySetValue,
])
const [value, setValue] = useState( const [value, setValue] = useState(
previouslySetValue?.valueText || defaultValue || '' previouslySetValue?.valueText || defaultValue || ''
) )
const [createNewVariable, setCreateNewVariable] = useState( const [createNewVariable, setCreateNewVariable] = useState(
(previouslySetValue && 'variableName' in previouslySetValue) || previouslySetValue && 'variableName' in previouslySetValue
arg.createVariableByDefault ||
false
) )
const [canSubmit, setCanSubmit] = useState(true) const [canSubmit, setCanSubmit] = useState(true)
useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' }))
@ -89,7 +52,10 @@ function CommandBarKclInput({
isNewVariableNameUnique, isNewVariableNameUnique,
} = useCalculateKclExpression({ } = useCalculateKclExpression({
value, value,
initialVariableName, initialVariableName:
previouslySetValue && 'variableName' in previouslySetValue
? previouslySetValue.variableName
: arg.name,
}) })
const varMentionData: Completion[] = prevVariables.map((v) => ({ const varMentionData: Completion[] = prevVariables.map((v) => ({
label: v.key, label: v.key,

View File

@ -1,4 +1,4 @@
import { APP_VERSION, RELEASE_URL } from 'routes/Settings' import { APP_VERSION } from 'routes/Settings'
import { CustomIcon } from 'components/CustomIcon' import { CustomIcon } from 'components/CustomIcon'
import Tooltip from 'components/Tooltip' import Tooltip from 'components/Tooltip'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
@ -72,8 +72,10 @@ export function LowerRightControls({
<menu className="flex items-center justify-end gap-3 pointer-events-auto"> <menu className="flex items-center justify-end gap-3 pointer-events-auto">
{!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />} {!location.pathname.startsWith(PATHS.HOME) && <ModelStateIndicator />}
<a <a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)} onClick={openExternalBrowserIfDesktop(
href={RELEASE_URL} `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={'!no-underline font-mono text-xs ' + linkOverrideClassName} className={'!no-underline font-mono text-xs ' + linkOverrideClassName}

View File

@ -41,10 +41,7 @@ import {
angleBetweenInfo, angleBetweenInfo,
applyConstraintAngleBetween, applyConstraintAngleBetween,
} from './Toolbar/SetAngleBetween' } from './Toolbar/SetAngleBetween'
import { import { applyConstraintAngleLength } from './Toolbar/setAngleLength'
applyConstraintAngleLength,
applyConstraintLength,
} from './Toolbar/setAngleLength'
import { import {
canSweepSelection, canSweepSelection,
handleSelectionBatch, handleSelectionBatch,
@ -54,7 +51,6 @@ import {
Selections, Selections,
updateSelections, updateSelections,
canLoftSelection, canLoftSelection,
canRevolveSelection,
canShellSelection, canShellSelection,
} from 'lib/selections' } from 'lib/selections'
import { applyConstraintIntersect } from './Toolbar/Intersect' import { applyConstraintIntersect } from './Toolbar/Intersect'
@ -67,13 +63,12 @@ import {
getSketchOrientationDetails, getSketchOrientationDetails,
} from 'clientSideScene/sceneEntities' } from 'clientSideScene/sceneEntities'
import { import {
insertNamedConstant, moveValueIntoNewVariablePath,
replaceValueAtNodePath,
sketchOnExtrudedFace, sketchOnExtrudedFace,
sketchOnOffsetPlane, sketchOnOffsetPlane,
startSketchOnDefault, startSketchOnDefault,
} from 'lang/modifyAst' } from 'lang/modifyAst'
import { PathToNode, Program, parse, recast, resultIsOk } from 'lang/wasm' import { Program, parse, recast, resultIsOk } from 'lang/wasm'
import { import {
doesSceneHaveExtrudedSketch, doesSceneHaveExtrudedSketch,
doesSceneHaveSweepableSketch, doesSceneHaveSweepableSketch,
@ -86,6 +81,7 @@ import toast from 'react-hot-toast'
import { EditorSelection, Transaction } from '@codemirror/state' import { EditorSelection, Transaction } from '@codemirror/state'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom' import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
import { getVarNameModal } from 'hooks/useToolbarGuards'
import { err, reportRejection, trap } from 'lib/trap' import { err, reportRejection, trap } from 'lib/trap'
import { useCommandsContext } from 'hooks/useCommandsContext' import { useCommandsContext } from 'hooks/useCommandsContext'
import { modelingMachineEvent } from 'editor/manager' import { modelingMachineEvent } from 'editor/manager'
@ -576,26 +572,6 @@ export const ModelingMachineProvider = ({
if (err(canSweep)) return false if (err(canSweep)) return false
return canSweep return canSweep
}, },
'has valid revolve selection': ({ context: { selectionRanges } }) => {
// A user can begin extruding if they either have 1+ faces selected or nothing selected
// TODO: I believe this guard only allows for extruding a single face at a time
const hasNoSelection =
selectionRanges.graphSelections.length === 0 ||
isRangeBetweenCharacters(selectionRanges) ||
isSelectionLastLine(selectionRanges, codeManager.code)
if (hasNoSelection) {
// they have no selection, we should enable the button
// so they can select the face through the cmdbar
// BUT only if there's extrudable geometry
return doesSceneHaveSweepableSketch(kclManager.ast)
}
if (!isSketchPipe(selectionRanges)) return false
const canSweep = canRevolveSelection(selectionRanges)
if (err(canSweep)) return false
return canSweep
},
'has valid loft selection': ({ context: { selectionRanges } }) => { 'has valid loft selection': ({ context: { selectionRanges } }) => {
const hasNoSelection = const hasNoSelection =
selectionRanges.graphSelections.length === 0 || selectionRanges.graphSelections.length === 0 ||
@ -913,18 +889,12 @@ export const ModelingMachineProvider = ({
} }
} }
), ),
astConstrainLength: fromPromise( 'Get length info': fromPromise(
async ({ async ({ input: { selectionRanges, sketchDetails } }) => {
input: { selectionRanges, sketchDetails, lengthValue }, const { modifiedAst, pathToNodeMap } =
}) => { await applyConstraintAngleLength({
if (!lengthValue) selectionRanges,
return Promise.reject(new Error('No length value')) })
const constraintResult = await applyConstraintLength({
selectionRanges,
length: lengthValue,
})
if (err(constraintResult)) return Promise.reject(constraintResult)
const { modifiedAst, pathToNodeMap } = constraintResult
const pResult = parse(recast(modifiedAst)) const pResult = parse(recast(modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
@ -1093,88 +1063,38 @@ export const ModelingMachineProvider = ({
} }
} }
), ),
'Apply named value constraint': fromPromise( 'Get convert to variable info': fromPromise(
async ({ input: { selectionRanges, sketchDetails, data } }) => { async ({ input: { selectionRanges, sketchDetails, data } }) => {
if (!sketchDetails) { if (!sketchDetails)
return Promise.reject(new Error('No sketch details')) return Promise.reject(new Error('No sketch details'))
} const { variableName } = await getVarNameModal({
if (!data) { valueName: data?.variableName || 'var',
return Promise.reject(new Error('No data from command flow')) })
}
let pResult = parse(recast(kclManager.ast)) let pResult = parse(recast(kclManager.ast))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
let parsed = pResult.program let parsed = pResult.program
let result: { const { modifiedAst: _modifiedAst, pathToReplacedNode } =
modifiedAst: Node<Program> moveValueIntoNewVariablePath(
pathToReplaced: PathToNode | null parsed,
} = { kclManager.programMemory,
modifiedAst: parsed, data?.pathToNode || [],
pathToReplaced: null, variableName
}
// If the user provided a constant name,
// we need to insert the named constant
// and then replace the node with the constant's name.
if ('variableName' in data.namedValue) {
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.variableName,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
const parseResultAfterInsertion = parse(
recast(
insertNamedConstant({
node: astAfterReplacement.modifiedAst,
newExpression: data.namedValue,
})
)
) )
if ( pResult = parse(recast(_modifiedAst))
trap(parseResultAfterInsertion) ||
!resultIsOk(parseResultAfterInsertion)
)
return Promise.reject(parseResultAfterInsertion)
result = {
modifiedAst: parseResultAfterInsertion.program,
pathToReplaced: astAfterReplacement.pathToReplaced,
}
} else if ('valueText' in data.namedValue) {
// If they didn't provide a constant name,
// just replace the node with the value.
const astAfterReplacement = replaceValueAtNodePath({
ast: parsed,
pathToNode: data.currentValue.pathToNode,
newExpressionString: data.namedValue.valueText,
})
if (trap(astAfterReplacement)) {
return Promise.reject(astAfterReplacement)
}
// The `replacer` function returns a pathToNode that assumes
// an identifier is also being inserted into the AST, creating an off-by-one error.
// This corrects that error, but TODO we should fix this upstream
// to avoid this kind of error in the future.
astAfterReplacement.pathToReplaced[1][0] =
(astAfterReplacement.pathToReplaced[1][0] as number) - 1
result = astAfterReplacement
}
pResult = parse(recast(result.modifiedAst))
if (trap(pResult) || !resultIsOk(pResult)) if (trap(pResult) || !resultIsOk(pResult))
return Promise.reject(new Error('Unexpected compilation error')) return Promise.reject(new Error('Unexpected compilation error'))
parsed = pResult.program parsed = pResult.program
if (trap(parsed)) return Promise.reject(parsed) if (trap(parsed)) return Promise.reject(parsed)
parsed = parsed as Node<Program> parsed = parsed as Node<Program>
if (!result.pathToReplaced) if (!pathToReplacedNode)
return Promise.reject(new Error('No path to replaced node')) return Promise.reject(new Error('No path to replaced node'))
const updatedAst = const updatedAst =
await sceneEntitiesManager.updateAstAndRejigSketch( await sceneEntitiesManager.updateAstAndRejigSketch(
result.pathToReplaced || [], pathToReplacedNode || [],
parsed, parsed,
sketchDetails.zAxis, sketchDetails.zAxis,
sketchDetails.yAxis, sketchDetails.yAxis,
@ -1187,7 +1107,7 @@ export const ModelingMachineProvider = ({
) )
const selection = updateSelections( const selection = updateSelections(
{ 0: result.pathToReplaced }, { 0: pathToReplacedNode },
selectionRanges, selectionRanges,
updatedAst.newAst updatedAst.newAst
) )
@ -1195,7 +1115,7 @@ export const ModelingMachineProvider = ({
return { return {
selectionType: 'completeSelection', selectionType: 'completeSelection',
selection, selection,
updatedPathToNode: result.pathToReplaced, updatedPathToNode: pathToReplacedNode,
} }
} }
), ),

View File

@ -76,7 +76,7 @@ export const ModelingPane = ({
return ( return (
<section <section
{...props} {...props}
aria-label={title && typeof title === 'string' ? title : ''} title={title && typeof title === 'string' ? title : ''}
data-testid={detailsTestId} data-testid={detailsTestId}
id={id} id={id}
className={ className={

View File

@ -10,7 +10,7 @@ interface AllKeybindingsFieldsProps {}
export const AllKeybindingsFields = forwardRef( export const AllKeybindingsFields = forwardRef(
( (
_props: AllKeybindingsFieldsProps, props: AllKeybindingsFieldsProps,
scrollRef: ForwardedRef<HTMLDivElement> scrollRef: ForwardedRef<HTMLDivElement>
) => { ) => {
// This is how we will get the interaction map from the context // This is how we will get the interaction map from the context
@ -25,7 +25,7 @@ export const AllKeybindingsFields = forwardRef(
.map(([category, categoryItems]) => ( .map(([category, categoryItems]) => (
<div className="flex flex-col gap-4 px-2 pr-4"> <div className="flex flex-col gap-4 px-2 pr-4">
<h2 <h2
id={`category-${category.replaceAll(/\s/g, '-')}`} id={`category-${category}`}
className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold" className="text-xl mt-6 first-of-type:mt-0 capitalize font-bold"
> >
{category} {category}

View File

@ -13,7 +13,7 @@ import { isDesktop } from 'lib/isDesktop'
import { ActionButton } from 'components/ActionButton' import { ActionButton } from 'components/ActionButton'
import { SettingsFieldInput } from './SettingsFieldInput' import { SettingsFieldInput } from './SettingsFieldInput'
import toast from 'react-hot-toast' import toast from 'react-hot-toast'
import { APP_VERSION, IS_NIGHTLY, RELEASE_URL } from 'routes/Settings' import { APP_VERSION, PACKAGE_NAME } from 'routes/Settings'
import { PATHS } from 'lib/paths' import { PATHS } from 'lib/paths'
import { import {
createAndOpenNewTutorialProject, createAndOpenNewTutorialProject,
@ -246,8 +246,10 @@ export const AllSettingsFields = forwardRef(
to inject the version from package.json */} to inject the version from package.json */}
App version {APP_VERSION}.{' '} App version {APP_VERSION}.{' '}
<a <a
onClick={openExternalBrowserIfDesktop(RELEASE_URL)} onClick={openExternalBrowserIfDesktop(
href={RELEASE_URL} `https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`
)}
href={`https://github.com/KittyCAD/modeling-app/releases/tag/v${APP_VERSION}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
@ -269,7 +271,7 @@ export const AllSettingsFields = forwardRef(
, and start a discussion if you don't see it! Your feedback will , and start a discussion if you don't see it! Your feedback will
help us prioritize what to build next. help us prioritize what to build next.
</p> </p>
{!IS_NIGHTLY && ( {PACKAGE_NAME.indexOf('-nightly') === -1 && (
<p className="max-w-2xl mt-6"> <p className="max-w-2xl mt-6">
Want to experience the latest and (hopefully) greatest from our Want to experience the latest and (hopefully) greatest from our
main development branch?{' '} main development branch?{' '}

View File

@ -19,7 +19,7 @@ export function KeybindingsSectionsList({
key={category} key={category}
onClick={() => onClick={() =>
scrollRef.current scrollRef.current
?.querySelector(`#category-${category.replaceAll(/\s/g, '-')}`) ?.querySelector(`#category-${category}`)
?.scrollIntoView({ ?.scrollIntoView({
block: 'center', block: 'center',
behavior: 'smooth', behavior: 'smooth',

View File

@ -22,7 +22,6 @@ import { removeDoubleNegatives } from '../AvailableVarsHelpers'
import { normaliseAngle } from '../../lib/utils' import { normaliseAngle } from '../../lib/utils'
import { kclManager } from 'lib/singletons' import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { KclCommandValue } from 'lib/commandTypes'
const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal) const getModalInfo = createSetAngleLengthModal(SetAngleLengthModal)
@ -64,57 +63,6 @@ export function angleLengthInfo({
return { enabled, transforms } return { enabled, transforms }
} }
export async function applyConstraintLength({
length,
selectionRanges,
}: {
length: KclCommandValue
selectionRanges: Selections
}) {
const ast = kclManager.ast
const angleLength = angleLengthInfo({ selectionRanges })
if (err(angleLength)) return angleLength
const { transforms } = angleLength
let distanceExpression: Expr = length.valueAst
/**
* To be "constrained", the value must be a binary expression, a named value, or a function call.
* If it has a variable name, we need to insert a variable declaration at the correct index.
*/
if (
'variableName' in length &&
length.variableName &&
length.insertIndex !== undefined
) {
const newBody = [...ast.body]
newBody.splice(length.insertIndex, 0, length.variableDeclarationAst)
ast.body = newBody
distanceExpression = createIdentifier(length.variableName)
}
if (!isExprBinaryPart(distanceExpression)) {
return new Error('Invalid valueNode, is not a BinaryPart')
}
const retval = transformAstSketchLines({
ast,
selectionRanges,
transformInfos: transforms,
programMemory: kclManager.programMemory,
referenceSegName: '',
forceValueUsedInTransform: distanceExpression,
})
if (err(retval)) return Promise.reject(retval)
const { modifiedAst: _modifiedAst, pathToNodeMap } = retval
return {
modifiedAst: _modifiedAst,
pathToNodeMap,
}
}
export async function applyConstraintAngleLength({ export async function applyConstraintAngleLength({
selectionRanges, selectionRanges,
angleOrLength = 'setLength', angleOrLength = 'setLength',

View File

@ -24,8 +24,6 @@ export function useConvertToVariable(range?: SourceRange) {
}, [enable]) }, [enable])
useEffect(() => { useEffect(() => {
// Return early if there are no selection ranges for whatever reason
if (!context.selectionRanges) return
const parsed = ast const parsed = ast
const meta = isNodeSafeToReplace( const meta = isNodeSafeToReplace(

View File

@ -45,7 +45,6 @@ import { TagDeclarator } from 'wasm-lib/kcl/bindings/TagDeclarator'
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { ExtrudeFacePlane } from 'machines/modelingMachine' import { ExtrudeFacePlane } from 'machines/modelingMachine'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { KclExpressionWithVariable } from 'lib/commandTypes'
export function startSketchOnDefault( export function startSketchOnDefault(
node: Node<Program>, node: Node<Program>,
@ -591,25 +590,6 @@ export function addOffsetPlane({
} }
} }
/**
* Return a modified clone of an AST with a named constant inserted into the body
*/
export function insertNamedConstant({
node,
newExpression,
}: {
node: Node<Program>
newExpression: KclExpressionWithVariable
}): Node<Program> {
const ast = structuredClone(node)
ast.body.splice(
newExpression.insertIndex,
0,
newExpression.variableDeclarationAst
)
return ast
}
/** /**
* Modify the AST to create a new sketch using the variable declaration * Modify the AST to create a new sketch using the variable declaration
* of an offset plane. The new sketch just has to come after the offset * of an offset plane. The new sketch just has to come after the offset
@ -953,31 +933,6 @@ export function giveSketchFnCallTag(
} }
} }
/**
* Replace a
*/
export function replaceValueAtNodePath({
ast,
pathToNode,
newExpressionString,
}: {
ast: Node<Program>
pathToNode: PathToNode
newExpressionString: string
}) {
const replaceCheckResult = isNodeSafeToReplacePath(ast, pathToNode)
if (err(replaceCheckResult)) {
return replaceCheckResult
}
const { isSafe, value, replacer } = replaceCheckResult
if (!isSafe || value.type === 'Identifier') {
return new Error('Not safe to replace')
}
return replacer(ast, newExpressionString)
}
export function moveValueIntoNewVariablePath( export function moveValueIntoNewVariablePath(
ast: Node<Program>, ast: Node<Program>,
programMemory: ProgramMemory, programMemory: ProgramMemory,

View File

@ -335,7 +335,7 @@ export function mutateAstWithTagForSketchSegment(
return { modifiedAst: astClone, tag } return { modifiedAst: astClone, tag }
} }
export function getEdgeTagCall( function getEdgeTagCall(
tag: string, tag: string,
artifact: Artifact artifact: Artifact
): Node<Identifier | CallExpression> { ): Node<Identifier | CallExpression> {

View File

@ -1,154 +0,0 @@
import { err } from 'lib/trap'
import { KCL_DEFAULT_CONSTANT_PREFIXES } from 'lib/constants'
import {
Program,
PathToNode,
Expr,
CallExpression,
PipeExpression,
VariableDeclarator,
} from 'lang/wasm'
import { Selections } from 'lib/selections'
import { Node } from 'wasm-lib/kcl/bindings/Node'
import {
createLiteral,
createCallExpressionStdLib,
createObjectExpression,
createIdentifier,
createPipeExpression,
findUniqueName,
createVariableDeclaration,
} from 'lang/modifyAst'
import { getNodeFromPath, getNodePathFromSourceRange } from 'lang/queryAst'
import {
mutateAstWithTagForSketchSegment,
getEdgeTagCall,
} from 'lang/modifyAst/addEdgeTreatment'
export function revolveSketch(
ast: Node<Program>,
pathToSketchNode: PathToNode,
shouldPipe = false,
angle: Expr = createLiteral(4),
axis: Selections
):
| {
modifiedAst: Node<Program>
pathToSketchNode: PathToNode
pathToRevolveArg: PathToNode
}
| Error {
const clonedAst = structuredClone(ast)
const sketchNode = getNodeFromPath(clonedAst, pathToSketchNode)
if (err(sketchNode)) return sketchNode
// testing code
const pathToAxisSelection = getNodePathFromSourceRange(
clonedAst,
axis.graphSelections[0]?.codeRef.range
)
const lineNode = getNodeFromPath<CallExpression>(
clonedAst,
pathToAxisSelection,
'CallExpression'
)
if (err(lineNode)) return lineNode
// TODO Kevin: What if |> close(%)?
// TODO Kevin: What if opposite edge
// TODO Kevin: What if the edge isn't planar to the sketch?
// TODO Kevin: add a tag.
const tagResult = mutateAstWithTagForSketchSegment(
clonedAst,
pathToAxisSelection
)
// Have the tag whether it is already created or a new one is generated
if (err(tagResult)) return tagResult
const { tag } = tagResult
/* Original Code */
const { node: sketchExpression } = sketchNode
// determine if sketchExpression is in a pipeExpression or not
const sketchPipeExpressionNode = getNodeFromPath<PipeExpression>(
clonedAst,
pathToSketchNode,
'PipeExpression'
)
if (err(sketchPipeExpressionNode)) return sketchPipeExpressionNode
const { node: sketchPipeExpression } = sketchPipeExpressionNode
const isInPipeExpression = sketchPipeExpression.type === 'PipeExpression'
const sketchVariableDeclaratorNode = getNodeFromPath<VariableDeclarator>(
clonedAst,
pathToSketchNode,
'VariableDeclarator'
)
if (err(sketchVariableDeclaratorNode)) return sketchVariableDeclaratorNode
const {
node: sketchVariableDeclarator,
shallowPath: sketchPathToDecleration,
} = sketchVariableDeclaratorNode
const axisSelection = axis?.graphSelections[0]?.artifact
if (!axisSelection) return new Error('Axis selection is missing.')
const revolveCall = createCallExpressionStdLib('revolve', [
createObjectExpression({
angle: angle,
axis: getEdgeTagCall(tag, axisSelection),
}),
createIdentifier(sketchVariableDeclarator.id.name),
])
if (shouldPipe) {
const pipeChain = createPipeExpression(
isInPipeExpression
? [...sketchPipeExpression.body, revolveCall]
: [sketchExpression as any, revolveCall]
)
sketchVariableDeclarator.init = pipeChain
const pathToRevolveArg: PathToNode = [
...sketchPathToDecleration,
['init', 'VariableDeclarator'],
['body', ''],
[pipeChain.body.length - 1, 'index'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode,
pathToRevolveArg,
}
}
// We're not creating a pipe expression,
// but rather a separate constant for the extrusion
const name = findUniqueName(clonedAst, KCL_DEFAULT_CONSTANT_PREFIXES.REVOLVE)
const VariableDeclaration = createVariableDeclaration(name, revolveCall)
const sketchIndexInPathToNode =
sketchPathToDecleration.findIndex((a) => a[0] === 'body') + 1
const sketchIndexInBody = sketchPathToDecleration[sketchIndexInPathToNode][0]
if (typeof sketchIndexInBody !== 'number')
return new Error('expected sketchIndexInBody to be a number')
clonedAst.body.splice(sketchIndexInBody + 1, 0, VariableDeclaration)
const pathToRevolveArg: PathToNode = [
['body', ''],
[sketchIndexInBody + 1, 'index'],
['declaration', 'VariableDeclaration'],
['init', 'VariableDeclarator'],
['arguments', 'CallExpression'],
[0, 'index'],
]
return {
modifiedAst: clonedAst,
pathToSketchNode: [...pathToSketchNode.slice(0, -1), [-1, 'index']],
pathToRevolveArg,
}
}

View File

@ -111,7 +111,8 @@ export function addShell({
const pathToNode: PathToNode = [ const pathToNode: PathToNode = [
['body', ''], ['body', ''],
[modifiedAst.body.length - 1, 'index'], [modifiedAst.body.length - 1, 'index'],
['declaration', 'VariableDeclaration'], ['declarations', 'VariableDeclaration'],
['0', 'index'],
['init', 'VariableDeclarator'], ['init', 'VariableDeclarator'],
['arguments', 'CallExpression'], ['arguments', 'CallExpression'],
[0, 'index'], [0, 'index'],

View File

@ -661,7 +661,7 @@ describe('Testing doesSceneHaveExtrudedSketch', () => {
|> circle({ center = [0, 0], radius = 1 }, %) |> circle({ center = [0, 0], radius = 1 }, %)
extrude001 = extrude(1, sketch001) extrude001 = extrude(1, sketch001)
` `
const ast = assertParse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast) const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy() expect(extrudable).toBeTruthy()
@ -671,7 +671,7 @@ extrude001 = extrude(1, sketch001)
|> circle({ center = [0, 0], radius = 1 }, %) |> circle({ center = [0, 0], radius = 1 }, %)
|> extrude(1, %) |> extrude(1, %)
` `
const ast = assertParse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast) const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeTruthy() expect(extrudable).toBeTruthy()
@ -680,7 +680,7 @@ extrude001 = extrude(1, sketch001)
const exampleCode = `extrude001 = startSketchOn('XZ') const exampleCode = `extrude001 = startSketchOn('XZ')
|> circle({ center = [0, 0], radius = 1 }, %) |> circle({ center = [0, 0], radius = 1 }, %)
` `
const ast = assertParse(exampleCode) const ast = parse(exampleCode)
if (err(ast)) throw ast if (err(ast)) throw ast
const extrudable = doesSceneHaveExtrudedSketch(ast) const extrudable = doesSceneHaveExtrudedSketch(ast)
expect(extrudable).toBeFalsy() expect(extrudable).toBeFalsy()

View File

@ -871,15 +871,3 @@ export function codeRefFromRange(range: SourceRange, ast: Program): CodeRef {
pathToNode: getNodePathFromSourceRange(ast, range), pathToNode: getNodePathFromSourceRange(ast, range),
} }
} }
export function isSolid2D(artifact: Artifact): artifact is solid2D {
return (artifact as solid2D).pathId !== undefined
}
export function isSegment(artifact: Artifact): artifact is SegmentArtifact {
return (artifact as SegmentArtifact).pathId !== undefined
}
export function isSweep(artifact: Artifact): artifact is SweepArtifact {
return (artifact as SweepArtifact).pathId !== undefined
}

View File

@ -1,13 +1,9 @@
import { err } from 'lib/trap' import { err } from 'lib/trap'
import { initPromise, parse, ParseResult } from './wasm' import { parse, ParseResult } from './wasm'
import { enginelessExecutor } from 'lib/testHelpers' import { enginelessExecutor } from 'lib/testHelpers'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Program } from '../wasm-lib/kcl/bindings/Program' import { Program } from '../wasm-lib/kcl/bindings/Program'
beforeEach(async () => {
await initPromise
})
it('can execute parsed AST', async () => { it('can execute parsed AST', async () => {
const code = `x = 1 const code = `x = 1
// A comment.` // A comment.`

View File

@ -32,14 +32,8 @@ export function mouseControlsToCameraSystem(
mouseControl: MouseControlType | undefined mouseControl: MouseControlType | undefined
): CameraSystem | undefined { ): CameraSystem | undefined {
switch (mouseControl) { switch (mouseControl) {
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'kittycad':
case 'kitty_cad': case 'kitty_cad':
return 'KittyCAD' return 'KittyCAD'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'onshape':
case 'on_shape': case 'on_shape':
return 'OnShape' return 'OnShape'
case 'trackpad_friendly': case 'trackpad_friendly':
@ -50,9 +44,6 @@ export function mouseControlsToCameraSystem(
return 'NX' return 'NX'
case 'creo': case 'creo':
return 'Creo' return 'Creo'
// TODO: understand why the values come back without underscores and fix the root cause
// @ts-ignore: TS2678
case 'autocad':
case 'auto_cad': case 'auto_cad':
return 'AutoCAD' return 'AutoCAD'
default: default:

View File

@ -1,15 +1,9 @@
import { Models } from '@kittycad/lib' import { Models } from '@kittycad/lib'
import { angleLengthInfo } from 'components/Toolbar/setAngleLength'
import { transformAstSketchLines } from 'lang/std/sketchcombos'
import { PathToNode } from 'lang/wasm'
import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes' import { StateMachineCommandSetConfig, KclCommandValue } from 'lib/commandTypes'
import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants' import { KCL_DEFAULT_LENGTH, KCL_DEFAULT_DEGREE } from 'lib/constants'
import { components } from 'lib/machine-api' import { components } from 'lib/machine-api'
import { Selections } from 'lib/selections' import { Selections } from 'lib/selections'
import { kclManager } from 'lib/singletons'
import { err } from 'lib/trap'
import { modelingMachine, SketchTool } from 'machines/modelingMachine' import { modelingMachine, SketchTool } from 'machines/modelingMachine'
import { revolveAxisValidator } from './validators'
type OutputFormat = Models['OutputFormat_type'] type OutputFormat = Models['OutputFormat_type']
type OutputTypeKey = OutputFormat['type'] type OutputTypeKey = OutputFormat['type']
@ -47,7 +41,6 @@ export type ModelingCommandSchema = {
Revolve: { Revolve: {
selection: Selections selection: Selections
angle: KclCommandValue angle: KclCommandValue
axis: Selections
} }
Fillet: { Fillet: {
// todo // todo
@ -61,18 +54,6 @@ export type ModelingCommandSchema = {
'change tool': { 'change tool': {
tool: SketchTool tool: SketchTool
} }
'Constrain length': {
selection: Selections
length: KclCommandValue
}
'Constrain with named value': {
currentValue: {
valueText: string
pathToNode: PathToNode
variableName: string
}
namedValue: KclCommandValue
}
'Text-to-CAD': { 'Text-to-CAD': {
prompt: string prompt: string
} }
@ -332,13 +313,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
required: true, required: true,
skip: true, skip: true,
}, },
axis: {
required: true,
inputType: 'selection',
selectionTypes: ['segment', 'sweepEdge', 'edgeCutEdge'],
multiple: false,
validation: revolveAxisValidator,
},
angle: { angle: {
inputType: 'kcl', inputType: 'kcl',
defaultValue: KCL_DEFAULT_DEGREE, defaultValue: KCL_DEFAULT_DEGREE,
@ -386,88 +360,6 @@ export const modelingMachineCommandConfig: StateMachineCommandSetConfig<
}, },
}, },
}, },
'Constrain length': {
description: 'Constrain the length of one or more segments.',
icon: 'dimension',
args: {
selection: {
inputType: 'selection',
selectionTypes: ['segment'],
multiple: false,
required: true,
skip: true,
},
length: {
inputType: 'kcl',
required: true,
createVariableByDefault: true,
defaultValue(_, machineContext) {
const selectionRanges = machineContext?.selectionRanges
if (!selectionRanges) return KCL_DEFAULT_LENGTH
const angleLength = angleLengthInfo({
selectionRanges,
angleOrLength: 'setLength',
})
if (err(angleLength)) return KCL_DEFAULT_LENGTH
const { transforms } = angleLength
// QUESTION: is it okay to reference kclManager here? will its state be up to date?
const sketched = transformAstSketchLines({
ast: structuredClone(kclManager.ast),
selectionRanges,
transformInfos: transforms,
programMemory: kclManager.programMemory,
referenceSegName: '',
})
if (err(sketched)) return KCL_DEFAULT_LENGTH
const { valueUsedInTransform } = sketched
return valueUsedInTransform?.toString() || KCL_DEFAULT_LENGTH
},
},
},
},
'Constrain with named value': {
description: 'Constrain a value by making it a named constant.',
icon: 'make-variable',
args: {
currentValue: {
description:
'Path to the node in the AST to constrain. This is never shown to the user.',
inputType: 'text',
required: false,
skip: true,
},
namedValue: {
inputType: 'kcl',
required: true,
createVariableByDefault: true,
variableName(commandBarContext, machineContext) {
const { currentValue } = commandBarContext.argumentsToSubmit
if (
!currentValue ||
!(currentValue instanceof Object) ||
!('variableName' in currentValue) ||
typeof currentValue.variableName !== 'string'
) {
return 'value'
}
return currentValue.variableName
},
defaultValue: (commandBarContext) => {
const { currentValue } = commandBarContext.argumentsToSubmit
if (
!currentValue ||
!(currentValue instanceof Object) ||
!('valueText' in currentValue) ||
typeof currentValue.valueText !== 'string'
) {
return KCL_DEFAULT_LENGTH
}
return currentValue.valueText
},
},
},
},
'Text-to-CAD': { 'Text-to-CAD': {
description: 'Use the Zoo Text-to-CAD API to generate part starters.', description: 'Use the Zoo Text-to-CAD API to generate part starters.',
icon: 'chat', icon: 'chat',

View File

@ -1,107 +0,0 @@
import { Models } from '@kittycad/lib'
import { engineCommandManager } from 'lib/singletons'
import { uuidv4 } from 'lib/utils'
import { CommandBarContext } from 'machines/commandBarMachine'
import { Selections } from 'lib/selections'
import { isSolid2D, isSegment, isSweep } from 'lang/std/artifactGraph'
export const disableDryRunWithRetry = async (numberOfRetries = 3) => {
for (let tries = 0; tries < numberOfRetries; tries++) {
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'disable_dry_run' },
})
// Exit out since the command was successful
return
} catch (e) {
console.error(e)
console.error('disable_dry_run failed. This is bad!')
}
}
}
// Takes a callback function and wraps it around enable_dry_run and disable_dry_run
export const dryRunWrapper = async (callback: () => Promise<any>) => {
// Gotcha: What about race conditions?
try {
await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: { type: 'enable_dry_run' },
})
const result = await callback()
return result
} catch (e) {
console.error(e)
} finally {
await disableDryRunWithRetry(5)
}
}
function isSelections(selections: unknown): selections is Selections {
return (
(selections as Selections).graphSelections !== undefined &&
(selections as Selections).otherSelections !== undefined
)
}
export const revolveAxisValidator = async ({
data,
context,
}: {
data: { [key: string]: Selections }
context: CommandBarContext
}): Promise<boolean | string> => {
if (!isSelections(context.argumentsToSubmit.selection)) {
return 'Unable to revolve, selections are missing'
}
const artifact =
context.argumentsToSubmit.selection.graphSelections[0].artifact
if (!artifact) {
return 'Unable to revolve, sketch not found'
}
if (!(isSolid2D(artifact) || isSegment(artifact) || isSweep(artifact))) {
return 'Unable to revolve, sketch has no path'
}
const sketchSelection = artifact.pathId
let edgeSelection = data.axis.graphSelections[0].artifact?.id
if (!sketchSelection) {
return 'Unable to revolve, sketch is missing'
}
if (!edgeSelection) {
return 'Unable to revolve, edge is missing'
}
const angleInDegrees: Models['Angle_type'] = {
unit: 'degrees',
value: 360,
}
const revolveAboutEdgeCommand = async () => {
return await engineCommandManager.sendSceneCommand({
type: 'modeling_cmd_req',
cmd_id: uuidv4(),
cmd: {
type: 'revolve_about_edge',
angle: angleInDegrees,
edge_id: edgeSelection,
target: sketchSelection,
tolerance: 0.0001,
},
})
}
const attemptRevolve = await dryRunWrapper(revolveAboutEdgeCommand)
if (attemptRevolve?.success) {
return true
} else {
// return error message for the toast
return 'Unable to revolve with selected axis'
}
}

View File

@ -7,7 +7,7 @@ import { ReactNode } from 'react'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import { Node } from 'wasm-lib/kcl/bindings/Node' import { Node } from 'wasm-lib/kcl/bindings/Node'
import { Artifact } from 'lang/std/artifactGraph' import { Artifact } from 'lang/std/artifactGraph'
import { CommandBarContext } from 'machines/commandBarMachine'
type Icon = CustomIconName type Icon = CustomIconName
const PLATFORMS = ['both', 'web', 'desktop'] as const const PLATFORMS = ['both', 'web', 'desktop'] as const
const INPUT_TYPES = [ const INPUT_TYPES = [
@ -147,30 +147,8 @@ export type CommandArgumentConfig<
inputType: 'selection' inputType: 'selection'
selectionTypes: Artifact['type'][] selectionTypes: Artifact['type'][]
multiple: boolean multiple: boolean
validation?: ({
data,
context,
}: {
data: any
context: CommandBarContext
}) => Promise<boolean | string>
}
| {
inputType: 'kcl'
createVariableByDefault?: boolean
variableName?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => string)
defaultValue?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: C
) => string)
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default values
| { | {
inputType: 'string' inputType: 'string'
defaultValue?: defaultValue?:
@ -243,30 +221,8 @@ export type CommandArgument<
inputType: 'selection' inputType: 'selection'
selectionTypes: Artifact['type'][] selectionTypes: Artifact['type'][]
multiple: boolean multiple: boolean
validation?: ({
data,
context,
}: {
data: any
context: CommandBarContext
}) => Promise<boolean | string>
}
| {
inputType: 'kcl'
createVariableByDefault?: boolean
variableName?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => string)
defaultValue?:
| string
| ((
commandBarContext: ContextFrom<typeof commandBarMachine>,
machineContext?: ContextFrom<T>
) => string)
} }
| { inputType: 'kcl'; defaultValue?: string } // KCL expression inputs have simple strings as default value
| { | {
inputType: 'string' inputType: 'string'
defaultValue?: defaultValue?:

View File

@ -111,10 +111,3 @@ export const KCL_SAMPLES_MANIFEST_URLS = {
/** Toast id for the app auto-updater toast */ /** Toast id for the app auto-updater toast */
export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast' export const AUTO_UPDATER_TOAST_ID = 'auto-updater-toast'
/** Local sketch axis values in KCL for operations, it could either be 'X' or 'Y' */
export const KCL_AXIS_X = 'X'
export const KCL_AXIS_Y = 'Y'
export const KCL_AXIS_NEG_X = '-X'
export const KCL_AXIS_NEG_Y = '-Y'
export const KCL_DEFAULT_AXIS = 'X'

View File

@ -155,8 +155,6 @@ export function buildCommandArgument<
context: ContextFrom<T>, context: ContextFrom<T>,
machineActor: Actor<T> machineActor: Actor<T>
): CommandArgument<O, T> & { inputType: typeof arg.inputType } { ): CommandArgument<O, T> & { inputType: typeof arg.inputType } {
// GOTCHA: modelingCommandConfig is not a 1:1 mapping to this baseCommandArgument
// You need to manually add key/value pairs here.
const baseCommandArgument = { const baseCommandArgument = {
description: arg.description, description: arg.description,
required: arg.required, required: arg.required,
@ -183,13 +181,10 @@ export function buildCommandArgument<
...baseCommandArgument, ...baseCommandArgument,
multiple: arg.multiple, multiple: arg.multiple,
selectionTypes: arg.selectionTypes, selectionTypes: arg.selectionTypes,
validation: arg.validation,
} satisfies CommandArgument<O, T> & { inputType: 'selection' } } satisfies CommandArgument<O, T> & { inputType: 'selection' }
} else if (arg.inputType === 'kcl') { } else if (arg.inputType === 'kcl') {
return { return {
inputType: arg.inputType, inputType: arg.inputType,
createVariableByDefault: arg.createVariableByDefault,
variableName: arg.variableName,
defaultValue: arg.defaultValue, defaultValue: arg.defaultValue,
...baseCommandArgument, ...baseCommandArgument,
} satisfies CommandArgument<O, T> & { inputType: 'kcl' } } satisfies CommandArgument<O, T> & { inputType: 'kcl' }

View File

@ -569,17 +569,6 @@ export function canSweepSelection(selection: Selections) {
) )
} }
export function canRevolveSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i)
)
return (
!!isSketchPipe(selection) &&
(commonNodes.every((n) => nodeHasClose(n)) ||
commonNodes.every((n) => nodeHasCircle(n)))
)
}
export function canLoftSelection(selection: Selections) { export function canLoftSelection(selection: Selections) {
const commonNodes = selection.graphSelections.map((_, i) => const commonNodes = selection.graphSelections.map((_, i) =>
buildCommonNodeFromSelection(selection, i) buildCommonNodeFromSelection(selection, i)
@ -641,29 +630,12 @@ export function getSelectionCountByType(
} }
}) })
selection.graphSelections.forEach((graphSelection) => { selection.graphSelections.forEach((selection) => {
if (!graphSelection.artifact) { if (!selection.artifact) {
/** incrementOrInitializeSelectionType('other')
* TODO: remove this heuristic-based selection type detection. return
* Currently, if you've created a sketch and have not left sketch mode,
* the selection will be a segment selection with no artifact.
* This is because the mock execution does not update the artifact graph.
* Once we move the artifactGraph creation to WASM, we can remove this,
* as the artifactGraph will always be up-to-date.
*/
if (isSingleCursorInPipe(selection, kclManager.ast)) {
incrementOrInitializeSelectionType('segment')
return
} else {
console.warn(
'Selection is outside of a sketch but has no artifact. Sketch segment selections are the only kind that can have a valid selection with no artifact.',
JSON.stringify(graphSelection)
)
incrementOrInitializeSelectionType('other')
return
}
} }
incrementOrInitializeSelectionType(graphSelection.artifact.type) incrementOrInitializeSelectionType(selection.artifact.type)
}) })
return selectionsByType return selectionsByType

View File

@ -12,7 +12,7 @@ export type InteractionMapItem = {
* Controls both the available names for interaction map categories * Controls both the available names for interaction map categories
* and the order in which they are displayed. * and the order in which they are displayed.
*/ */
const interactionMapCategories = [ export const interactionMapCategories = [
'Sketching', 'Sketching',
'Modeling', 'Modeling',
'Command Palette', 'Command Palette',

View File

@ -540,15 +540,13 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[ [
{ {
id: 'constraint-length', id: 'constraint-length',
disabled: (state) => !state.matches({ Sketch: 'SketchIdle' }), disabled: (state) =>
onClick: ({ commandBarSend }) => !(
commandBarSend({ state.matches({ Sketch: 'SketchIdle' }) &&
type: 'Find and select command', state.can({ type: 'Constrain length' })
data: { ),
name: 'Constrain length', onClick: ({ modelingSend }) =>
groupId: 'modeling', modelingSend({ type: 'Constrain length' }),
},
}),
icon: 'dimension', icon: 'dimension',
status: 'available', status: 'available',
title: 'Length', title: 'Length',

View File

@ -8,7 +8,6 @@ import {
import { Selections__old } from 'lib/selections' import { Selections__old } from 'lib/selections'
import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils' import { getCommandArgumentKclValuesOnly } from 'lib/commandUtils'
import { MachineManager } from 'components/MachineManagerProvider' import { MachineManager } from 'components/MachineManagerProvider'
import toast from 'react-hot-toast'
export type CommandBarContext = { export type CommandBarContext = {
commands: Command[] commands: Command[]
@ -248,69 +247,14 @@ export const commandBarMachine = setup({
'All arguments are skippable': () => false, 'All arguments are skippable': () => false,
}, },
actors: { actors: {
'Validate argument': fromPromise( 'Validate argument': fromPromise(({ input }) => {
({ return new Promise((resolve, reject) => {
input, // TODO: figure out if we should validate argument data here or in the form itself,
}: { // and if we should support people configuring a argument's validation function
input: {
context: CommandBarContext | undefined
event: CommandBarMachineEvent | undefined
}
}) => {
return new Promise((resolve, reject) => {
if (!input || input?.event?.type !== 'Submit argument') {
toast.error(`Unable to validate, wrong event type.`)
return reject(`Unable to validate, wrong event type`)
}
const context = input?.context resolve(input)
})
if (!context) { }),
toast.error(`Unable to validate, wrong argument.`)
return reject(`Unable to validate, wrong argument`)
}
const data = input.event.data
const argName = context.currentArgument?.name
const args = context?.selectedCommand?.args
const argConfig = args && argName ? args[argName] : undefined
// Only do a validation check if the argument, selectedCommand, and the validation function are defined
if (
context.currentArgument &&
context.selectedCommand &&
argConfig?.inputType === 'selection' &&
argConfig?.validation
) {
argConfig
.validation({ context, data })
.then((result) => {
if (typeof result === 'boolean' && result === true) {
return resolve(data)
} else {
// validation failed
if (typeof result === 'string') {
// The result of the validation is the error message
toast.error(result)
return reject(
`unable to validate ${argName}, Message: ${result}`
)
} else {
// Default message if there is not a custom one sent
toast.error(`Unable to validate ${argName}`)
return reject(`unable to validate ${argName}}`)
}
}
})
.catch(() => {
return reject(`unable to validate ${argName}}`)
})
} else {
// Missing several requirements for validate argument, just bypass
return resolve(data)
}
})
}
),
'Validate all arguments': fromPromise( 'Validate all arguments': fromPromise(
({ input }: { input: CommandBarContext }) => { ({ input }: { input: CommandBarContext }) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -505,10 +449,9 @@ export const commandBarMachine = setup({
invoke: { invoke: {
src: 'Validate argument', src: 'Validate argument',
id: 'validateSingleArgument', id: 'validateSingleArgument',
input: ({ event, context }) => { input: ({ event }) => {
if (event.type !== 'Submit argument') if (event.type !== 'Submit argument') return {}
return { event: undefined, context: undefined } return event.data
return { event, context }
}, },
onDone: { onDone: {
target: '#Command Bar.Checking Arguments', target: '#Command Bar.Checking Arguments',

File diff suppressed because one or more lines are too long

View File

@ -23,15 +23,6 @@ import argvFromYargs from './commandLineArgs'
let mainWindow: BrowserWindow | null = null let mainWindow: BrowserWindow | null = null
// Supporting multiple instances instead of multiple applications
let cmdQPressed = false
const instances: BrowserWindow[] = []
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
app.quit()
process.exit(0)
}
// Check the command line arguments for a project path // Check the command line arguments for a project path
const args = parseCLIArgs() const args = parseCLIArgs()
@ -126,34 +117,16 @@ const createWindow = (filePath?: string): BrowserWindow => {
newWindow.show() newWindow.show()
instances.push(newWindow)
return newWindow return newWindow
} }
// before-quit with multiple instances
if (process.platform === 'darwin') {
// Quit from the dock context menu should quit the application directly
app.on('before-quit', () => {
cmdQPressed = true
})
}
// Quit when all windows are closed, even on macOS. There, it's common // Quit when all windows are closed, even on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q, but it is a really weird behavior with our app. // explicitly with Cmd + Q, but it is a really weird behavior with our app.
// app.on('window-all-closed', () => {
// app.quit()
// })
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
if (cmdQPressed || process.platform !== 'darwin') { app.quit()
app.quit()
}
}) })
// Various actions can trigger this event, such as launching the application for the first time,
// attempting to re-launch the application when it's already running, or clicking on the application's dock or taskbar icon.
app.on('activate', () => createWindow())
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs. // Some APIs can only be used after this event occurs.
@ -162,10 +135,6 @@ app.on('ready', (event, data) => {
mainWindow = createWindow() mainWindow = createWindow()
}) })
// This event will be emitted inside the primary instance of your application when a second instance
// has been executed and calls app.requestSingleInstanceLock().
app.on('second-instance', (event, argv, workingDirectory) => createWindow())
// For now there is no good reason to separate these out to another file(s) // For now there is no good reason to separate these out to another file(s)
// There is just not enough code to warrant it and further abstracts everything // There is just not enough code to warrant it and further abstracts everything
// which is already quite abstracted // which is already quite abstracted

View File

@ -30,12 +30,6 @@ export const PACKAGE_NAME = isDesktop()
? window.electron.packageJson.name ? window.electron.packageJson.name
: 'zoo-modeling-app' : 'zoo-modeling-app'
export const IS_NIGHTLY = PACKAGE_NAME.indexOf('-nightly') > -1
export const RELEASE_URL = `https://github.com/KittyCAD/modeling-app/releases/tag/${
IS_NIGHTLY ? 'nightly-' : ''
}v${APP_VERSION}`
export const Settings = () => { export const Settings = () => {
const navigate = useNavigate() const navigate = useNavigate()
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()

View File

@ -79,10 +79,7 @@ kittycad = { version = "0.3.28", default-features = false, features = ["js", "re
kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] } kittycad-modeling-cmds = { version = "0.2.77", features = ["websocket"] }
[workspace.lints.clippy] [workspace.lints.clippy]
assertions_on_result_states = "warn"
dbg_macro = "warn"
iter_over_hash_type = "warn" iter_over_hash_type = "warn"
lossy_float_literal = "warn"
[[test]] [[test]]
name = "executor" name = "executor"

View File

@ -12,8 +12,8 @@ redo-kcl-stdlib-docs-no-imgs:
# Generate the stdlib image artifacts # Generate the stdlib image artifacts
# Then run the stdlib docs generation # Then run the stdlib docs generation
redo-kcl-stdlib-docs: redo-kcl-stdlib-docs:
TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- kcl_test_example TWENTY_TWENTY=overwrite {{cnr}} -p kcl-lib kcl_test_example
EXPECTORATE=overwrite {{cnr}} -p kcl-lib --no-fail-fast -- docs::gen_std_tests::test_generate_stdlib EXPECTORATE=overwrite {{cnr}} -p kcl-lib docs::gen_std_tests::test_generate_stdlib
# Copy a test KCL file from executor tests into a new simulation test. # Copy a test KCL file from executor tests into a new simulation test.
copy-exec-test-into-sim-test test_name: copy-exec-test-into-sim-test test_name:

View File

@ -15,5 +15,5 @@ async fn kcl_to_core_test() {
) )
.await; .await;
result.unwrap(); assert!(result.is_ok());
} }

View File

@ -366,11 +366,9 @@ impl Node<CallExpressionKw> {
#[async_recursion] #[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let fn_name = &self.callee.name; let fn_name = &self.callee.name;
let callsite: SourceRange = self.into();
// Build a hashmap from argument labels to the final evaluated values. // Build a hashmap from argument labels to the final evaluated values.
let mut fn_args = HashMap::with_capacity(self.arguments.len()); let mut fn_args = HashMap::with_capacity(self.arguments.len());
let mut tag_declarator_args = Vec::new();
for arg_expr in &self.arguments { for arg_expr in &self.arguments {
let source_range = SourceRange::from(arg_expr.arg.clone()); let source_range = SourceRange::from(arg_expr.arg.clone());
let metadata = Metadata { source_range }; let metadata = Metadata { source_range };
@ -378,12 +376,8 @@ impl Node<CallExpressionKw> {
.execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression) .execute_expr(&arg_expr.arg, exec_state, &metadata, StatementKind::Expression)
.await?; .await?;
fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range)); fn_args.insert(arg_expr.label.name.clone(), Arg::new(value, source_range));
if let Expr::TagDeclarator(td) = &arg_expr.arg {
tag_declarator_args.push((td.inner.clone(), source_range));
}
} }
let fn_args = fn_args; // remove mutability let fn_args = fn_args; // remove mutability
let tag_declarator_args = tag_declarator_args; // remove mutability
// Evaluate the unlabeled first param, if any exists. // Evaluate the unlabeled first param, if any exists.
let unlabeled = if let Some(ref arg_expr) = self.unlabeled { let unlabeled = if let Some(ref arg_expr) = self.unlabeled {
@ -409,43 +403,11 @@ impl Node<CallExpressionKw> {
FunctionKind::Core(func) => { FunctionKind::Core(func) => {
// Attempt to call the function. // Attempt to call the function.
let mut result = func.std_lib_fn()(exec_state, args).await?; let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?; update_memory_for_tags_of_geometry(&mut result, exec_state)?;
Ok(result) Ok(result)
} }
FunctionKind::UserDefined => { FunctionKind::UserDefined => {
let source_range = SourceRange::from(self); todo!("Part of modeling-app#4600: Support keyword arguments for user-defined functions")
// Clone the function so that we can use a mutable reference to
// exec_state.
let func = exec_state.memory.get(fn_name, source_range)?.clone();
let fn_dynamic_state = exec_state.dynamic_state.merge(&exec_state.memory);
let return_value = {
let previous_dynamic_state = std::mem::replace(&mut exec_state.dynamic_state, fn_dynamic_state);
let result = func
.call_fn_kw(args, exec_state, ctx.clone(), callsite)
.await
.map_err(|e| {
// Add the call expression to the source ranges.
// TODO currently ignored by the frontend
e.add_source_ranges(vec![source_range])
});
exec_state.dynamic_state = previous_dynamic_state;
result?
};
let result = return_value.ok_or_else(move || {
let mut source_ranges: Vec<SourceRange> = vec![source_range];
// We want to send the source range of the original function.
if let KclValue::Function { meta, .. } = func {
source_ranges = meta.iter().map(|m| m.source_range).collect();
};
KclError::UndefinedValue(KclErrorDetails {
message: format!("Result of user-defined function {} is undefined", fn_name),
source_ranges,
})
})?;
Ok(result)
} }
} }
} }
@ -457,7 +419,6 @@ impl Node<CallExpression> {
let fn_name = &self.callee.name; let fn_name = &self.callee.name;
let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len()); let mut fn_args: Vec<Arg> = Vec::with_capacity(self.arguments.len());
let mut tag_declarator_args = Vec::new();
for arg_expr in &self.arguments { for arg_expr in &self.arguments {
let metadata = Metadata { let metadata = Metadata {
@ -467,19 +428,15 @@ impl Node<CallExpression> {
.execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression) .execute_expr(arg_expr, exec_state, &metadata, StatementKind::Expression)
.await?; .await?;
let arg = Arg::new(value, SourceRange::from(arg_expr)); let arg = Arg::new(value, SourceRange::from(arg_expr));
if let Expr::TagDeclarator(td) = arg_expr {
tag_declarator_args.push((td.inner.clone(), arg.source_range));
}
fn_args.push(arg); fn_args.push(arg);
} }
let tag_declarator_args = tag_declarator_args; // remove mutability
match ctx.stdlib.get_either(fn_name) { match ctx.stdlib.get_either(fn_name) {
FunctionKind::Core(func) => { FunctionKind::Core(func) => {
// Attempt to call the function. // Attempt to call the function.
let args = crate::std::Args::new(fn_args, self.into(), ctx.clone()); let args = crate::std::Args::new(fn_args, self.into(), ctx.clone());
let mut result = func.std_lib_fn()(exec_state, args).await?; let mut result = func.std_lib_fn()(exec_state, args).await?;
update_memory_for_tags_of_geometry(&mut result, &tag_declarator_args, exec_state)?; update_memory_for_tags_of_geometry(&mut result, exec_state)?;
Ok(result) Ok(result)
} }
FunctionKind::UserDefined => { FunctionKind::UserDefined => {
@ -518,24 +475,7 @@ impl Node<CallExpression> {
} }
} }
/// `tag_declarator_args` should only contain tag declarator literals, which fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
/// will be defined as local variables. Non-literals that evaluate to tag
/// declarators should not be defined.
fn update_memory_for_tags_of_geometry(
result: &mut KclValue,
tag_declarator_args: &[(TagDeclarator, SourceRange)],
exec_state: &mut ExecState,
) -> Result<(), KclError> {
// Define all the tags in the memory.
for (tag_declarator, arg_sr) in tag_declarator_args {
let tag = TagIdentifier {
value: tag_declarator.name.clone(),
info: None,
meta: vec![Metadata { source_range: *arg_sr }],
};
exec_state.memory.add_tag(&tag.value, tag.clone(), *arg_sr)?;
}
// If the return result is a sketch or solid, we want to update the // If the return result is a sketch or solid, we want to update the
// memory for the tags of the group. // memory for the tags of the group.
// TODO: This could probably be done in a better way, but as of now this was my only idea // TODO: This could probably be done in a better way, but as of now this was my only idea
@ -543,7 +483,7 @@ fn update_memory_for_tags_of_geometry(
match result { match result {
KclValue::Sketch { value: ref mut sketch } => { KclValue::Sketch { value: ref mut sketch } => {
for (_, tag) in sketch.tags.iter() { for (_, tag) in sketch.tags.iter() {
exec_state.memory.update_tag_if_defined(&tag.value, tag.clone()); exec_state.memory.update_tag(&tag.value, tag.clone())?;
} }
} }
KclValue::Solid(ref mut solid) => { KclValue::Solid(ref mut solid) => {
@ -581,7 +521,7 @@ fn update_memory_for_tags_of_geometry(
info.sketch = solid.id; info.sketch = solid.id;
t.info = Some(info); t.info = Some(info);
exec_state.memory.update_tag_if_defined(&tag.name, t.clone()); exec_state.memory.update_tag(&tag.name, t.clone())?;
// update the sketch tags. // update the sketch tags.
solid.sketch.tags.insert(tag.name.clone(), t); solid.sketch.tags.insert(tag.name.clone(), t);
@ -602,6 +542,22 @@ fn update_memory_for_tags_of_geometry(
Ok(()) Ok(())
} }
impl Node<TagDeclarator> {
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
value: self.name.clone(),
info: None,
meta: vec![Metadata {
source_range: self.into(),
}],
}));
exec_state.memory.add(&self.name, memory_item.clone(), self.into())?;
Ok(self.into())
}
}
impl Node<ArrayExpression> { impl Node<ArrayExpression> {
#[async_recursion] #[async_recursion]
pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> { pub async fn execute(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {

View File

@ -72,10 +72,6 @@ pub enum KclValue {
ImportedGeometry(ImportedGeometry), ImportedGeometry(ImportedGeometry),
#[ts(skip)] #[ts(skip)]
Function { Function {
/// Adam Chalmers speculation:
/// Reference to a KCL stdlib function (written in Rust).
/// Some if the KCL value is an alias of a stdlib function,
/// None if it's a KCL function written/declared in KCL.
#[serde(skip)] #[serde(skip)]
func: Option<MemoryFunction>, func: Option<MemoryFunction>,
#[schemars(skip)] #[schemars(skip)]
@ -507,39 +503,4 @@ impl KclValue {
.await .await
} }
} }
/// If this is a function, call it by applying keyword arguments.
/// If it's not a function, returns an error.
pub async fn call_fn_kw(
&self,
args: crate::std::Args,
exec_state: &mut ExecState,
ctx: ExecutorContext,
callsite: SourceRange,
) -> Result<Option<KclValue>, KclError> {
let KclValue::Function {
func,
expression,
memory: closure_memory,
meta: _,
} = &self
else {
return Err(KclError::Semantic(KclErrorDetails {
message: "cannot call this because it isn't a function".to_string(),
source_ranges: vec![callsite],
}));
};
if let Some(_func) = func {
todo!("Implement calling KCL stdlib fns that are aliased. Part of https://github.com/KittyCAD/modeling-app/issues/4600");
} else {
crate::execution::call_user_defined_function_kw(
args.kw_args,
closure_memory.as_ref(),
expression.as_ref(),
exec_state,
&ctx,
)
.await
}
}
} }

View File

@ -125,16 +125,10 @@ impl ProgramMemory {
Ok(()) Ok(())
} }
pub fn add_tag(&mut self, tag: &str, value: TagIdentifier, source_range: SourceRange) -> Result<(), KclError> { pub fn update_tag(&mut self, tag: &str, value: TagIdentifier) -> Result<(), KclError> {
self.add(tag, KclValue::TagIdentifier(Box::new(value)), source_range)
}
pub fn update_tag_if_defined(&mut self, tag: &str, value: TagIdentifier) {
if !self.environments[self.current_env.index()].contains_key(tag) {
// Do nothing if the tag isn't defined.
return;
}
self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value))); self.environments[self.current_env.index()].insert(tag.to_string(), KclValue::TagIdentifier(Box::new(value)));
Ok(())
} }
/// Get a value from the program memory. /// Get a value from the program memory.
@ -851,7 +845,7 @@ impl GetTangentialInfoFromPathsResult {
impl Sketch { impl Sketch {
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) { pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
let mut tag_identifier = TagIdentifier::from(tag); let mut tag_identifier: TagIdentifier = tag.into();
let base = current_path.get_base(); let base = current_path.get_base();
tag_identifier.info = Some(TagEngineInfo { tag_identifier.info = Some(TagEngineInfo {
id: base.geo_meta.id, id: base.geo_meta.id,
@ -2091,7 +2085,7 @@ impl ExecutorContext {
let item = match init { let item = match init {
Expr::None(none) => KclValue::from(none), Expr::None(none) => KclValue::from(none),
Expr::Literal(literal) => KclValue::from(literal), Expr::Literal(literal) => KclValue::from(literal),
Expr::TagDeclarator(tag) => KclValue::from(tag), Expr::TagDeclarator(tag) => tag.execute(exec_state).await?,
Expr::Identifier(identifier) => { Expr::Identifier(identifier) => {
let value = exec_state.memory.get(&identifier.name, identifier.into())?; let value = exec_state.memory.get(&identifier.name, identifier.into())?;
value.clone() value.clone()
@ -2253,59 +2247,6 @@ fn assign_args_to_params(
Ok(fn_memory) Ok(fn_memory)
} }
fn assign_args_to_params_kw(
function_expression: NodeRef<'_, FunctionExpression>,
mut args: crate::std::args::KwArgs,
mut fn_memory: ProgramMemory,
) -> Result<ProgramMemory, KclError> {
// Add the arguments to the memory. A new call frame should have already
// been created.
let source_ranges = vec![function_expression.into()];
for param in function_expression.params.iter() {
if param.labeled {
let arg = args.labeled.get(&param.identifier.name);
let arg_val = match arg {
Some(arg) => arg.value.clone(),
None => match param.default_value {
Some(ref default_val) => KclValue::from(default_val.clone()),
None => {
return Err(KclError::Semantic(KclErrorDetails {
source_ranges,
message: format!(
"This function requires a parameter {}, but you haven't passed it one.",
param.identifier.name
),
}));
}
},
};
fn_memory.add(&param.identifier.name, arg_val, (&param.identifier).into())?;
} else {
let Some(unlabeled) = args.unlabeled.take() else {
let param_name = &param.identifier.name;
return Err(if args.labeled.contains_key(param_name) {
KclError::Semantic(KclErrorDetails {
source_ranges,
message: format!("The function does declare a parameter named '{param_name}', but this parameter doesn't use a label. Try removing the `{param_name}:`"),
})
} else {
KclError::Semantic(KclErrorDetails {
source_ranges,
message: "This function expects an unlabeled first parameter, but you haven't passed it one."
.to_owned(),
})
});
};
fn_memory.add(
&param.identifier.name,
unlabeled.value.clone(),
(&param.identifier).into(),
)?;
}
}
Ok(fn_memory)
}
pub(crate) async fn call_user_defined_function( pub(crate) async fn call_user_defined_function(
args: Vec<Arg>, args: Vec<Arg>,
memory: &ProgramMemory, memory: &ProgramMemory,
@ -2336,36 +2277,6 @@ pub(crate) async fn call_user_defined_function(
result.map(|_| fn_memory.return_) result.map(|_| fn_memory.return_)
} }
pub(crate) async fn call_user_defined_function_kw(
args: crate::std::args::KwArgs,
memory: &ProgramMemory,
function_expression: NodeRef<'_, FunctionExpression>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<Option<KclValue>, KclError> {
// Create a new environment to execute the function body in so that local
// variables shadow variables in the parent scope. The new environment's
// parent should be the environment of the closure.
let mut body_memory = memory.clone();
let body_env = body_memory.new_env_for_call(memory.current_env);
body_memory.current_env = body_env;
let fn_memory = assign_args_to_params_kw(function_expression, args, body_memory)?;
// Execute the function body using the memory we just created.
let (result, fn_memory) = {
let previous_memory = std::mem::replace(&mut exec_state.memory, fn_memory);
let result = ctx
.inner_execute(&function_expression.body, exec_state, BodyType::Block)
.await;
// Restore the previous memory.
let fn_memory = std::mem::replace(&mut exec_state.memory, previous_memory);
(result, fn_memory)
};
result.map(|_| fn_memory.return_)
}
pub enum StatementKind<'a> { pub enum StatementKind<'a> {
Declaration { name: &'a str }, Declaration { name: &'a str },
Expression, Expression,
@ -2977,10 +2888,8 @@ let notTagDeclarator = !myTagDeclarator";
); );
let code9 = " let code9 = "
sk = startSketchOn('XY') let myTagDeclarator = $myTag
|> startProfileAt([0, 0], %) let notTagIdentifier = !myTag";
|> line([5, 0], %, $myTag)
notTagIdentifier = !myTag";
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap(); let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to // These are currently printed out as JSON objects, so we don't want to
// check the full error. // check the full error.

View File

@ -138,7 +138,7 @@ pub use lsp::test_util::kcl_lsp_server;
impl Program { impl Program {
pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> { pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let tokens = parsing::token::lex(input, module_id)?; let tokens = parsing::token::lexer(input, module_id)?;
let (ast, errs) = parsing::parse_tokens(tokens).0?; let (ast, errs) = parsing::parse_tokens(tokens).0?;
Ok((ast.map(|ast| Program { ast }), errs)) Ok((ast.map(|ast| Program { ast }), errs))
@ -146,7 +146,7 @@ impl Program {
pub fn parse_no_errs(input: &str) -> Result<Program, KclError> { pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let tokens = parsing::token::lex(input, module_id)?; let tokens = parsing::token::lexer(input, module_id)?;
let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?; let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?;
Ok(Program { ast }) Ok(Program { ast })

View File

@ -49,31 +49,34 @@ use crate::{
cache::{CacheInformation, OldAstState}, cache::{CacheInformation, OldAstState},
types::{Expr, Node, VariableKind}, types::{Expr, Node, VariableKind},
}, },
token::TokenStream, token::TokenType,
PIPE_OPERATOR, PIPE_OPERATOR,
}, },
ModuleId, Program, SourceRange, ModuleId, Program, SourceRange,
}; };
const SEMANTIC_TOKEN_TYPES: [SemanticTokenType; 10] = [
SemanticTokenType::NUMBER,
SemanticTokenType::VARIABLE,
SemanticTokenType::KEYWORD,
SemanticTokenType::TYPE,
SemanticTokenType::STRING,
SemanticTokenType::OPERATOR,
SemanticTokenType::COMMENT,
SemanticTokenType::FUNCTION,
SemanticTokenType::PARAMETER,
SemanticTokenType::PROPERTY,
];
const SEMANTIC_TOKEN_MODIFIERS: [SemanticTokenModifier; 5] = [ lazy_static::lazy_static! {
SemanticTokenModifier::DECLARATION, pub static ref SEMANTIC_TOKEN_TYPES: Vec<SemanticTokenType> = {
SemanticTokenModifier::DEFINITION, // This is safe to unwrap because we know all the token types are valid.
SemanticTokenModifier::DEFAULT_LIBRARY, // And the test would fail if they were not.
SemanticTokenModifier::READONLY, let mut gen = TokenType::all_semantic_token_types().unwrap();
SemanticTokenModifier::STATIC, gen.extend(vec![
]; SemanticTokenType::PARAMETER,
SemanticTokenType::PROPERTY,
]);
gen
};
pub static ref SEMANTIC_TOKEN_MODIFIERS: Vec<SemanticTokenModifier> = {
vec![
SemanticTokenModifier::DECLARATION,
SemanticTokenModifier::DEFINITION,
SemanticTokenModifier::DEFAULT_LIBRARY,
SemanticTokenModifier::READONLY,
SemanticTokenModifier::STATIC,
]
};
}
/// A subcommand for running the server. /// A subcommand for running the server.
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -102,7 +105,7 @@ pub struct Backend {
/// The stdlib signatures for the language. /// The stdlib signatures for the language.
pub stdlib_signatures: HashMap<String, SignatureHelp>, pub stdlib_signatures: HashMap<String, SignatureHelp>,
/// Token maps. /// Token maps.
pub(super) token_map: DashMap<String, TokenStream>, pub token_map: DashMap<String, Vec<crate::parsing::token::Token>>,
/// AST maps. /// AST maps.
pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>, pub ast_map: DashMap<String, Node<crate::parsing::ast::types::Program>>,
/// Last successful execution. /// Last successful execution.
@ -281,7 +284,7 @@ impl crate::lsp::backend::Backend for Backend {
// Lets update the tokens. // Lets update the tokens.
let module_id = ModuleId::default(); let module_id = ModuleId::default();
let tokens = match crate::parsing::token::lex(&params.text, module_id) { let tokens = match crate::parsing::token::lexer(&params.text, module_id) {
Ok(tokens) => tokens, Ok(tokens) => tokens,
Err(err) => { Err(err) => {
self.add_to_diagnostics(&params, &[err], true).await; self.add_to_diagnostics(&params, &[err], true).await;
@ -407,11 +410,11 @@ impl Backend {
self.executor_ctx.read().await self.executor_ctx.read().await
} }
async fn update_semantic_tokens(&self, tokens: &TokenStream, params: &TextDocumentItem) { async fn update_semantic_tokens(&self, tokens: &[crate::parsing::token::Token], params: &TextDocumentItem) {
// Update the semantic tokens map. // Update the semantic tokens map.
let mut semantic_tokens = vec![]; let mut semantic_tokens = vec![];
let mut last_position = Position::new(0, 0); let mut last_position = Position::new(0, 0);
for token in tokens.as_slice() { for token in tokens {
let Ok(token_type) = SemanticTokenType::try_from(token.token_type) else { let Ok(token_type) = SemanticTokenType::try_from(token.token_type) else {
// We continue here because not all tokens can be converted this way, we will get // We continue here because not all tokens can be converted this way, we will get
// the rest from the ast. // the rest from the ast.
@ -441,11 +444,8 @@ impl Backend {
let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) { let token_modifiers_bitset = if let Some(ast) = self.ast_map.get(params.uri.as_str()) {
let token_index = Arc::new(Mutex::new(token_type_index)); let token_index = Arc::new(Mutex::new(token_type_index));
let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0)); let modifier_index: Arc<Mutex<u32>> = Arc::new(Mutex::new(0));
crate::walk::walk(&ast, |node: crate::walk::Node| { crate::walk::walk(&ast, &|node: crate::walk::Node| {
let Ok(node_range): Result<SourceRange, _> = (&node).try_into() else { let node_range: SourceRange = (&node).into();
return Ok(true);
};
if !node_range.contains(source_range.start()) { if !node_range.contains(source_range.start()) {
return Ok(true); return Ok(true);
} }
@ -563,7 +563,7 @@ impl Backend {
let semantic_token = SemanticToken { let semantic_token = SemanticToken {
delta_line: position.line - last_position.line + 1, delta_line: position.line - last_position.line + 1,
delta_start: 0, delta_start: 0,
length: (token.end - token.start) as u32, length: token.value.len() as u32,
token_type: token_type_index, token_type: token_type_index,
token_modifiers_bitset, token_modifiers_bitset,
}; };
@ -582,7 +582,7 @@ impl Backend {
} else { } else {
position.character - last_position.character position.character - last_position.character
}, },
length: (token.end - token.start) as u32, length: token.value.len() as u32,
token_type: token_type_index, token_type: token_type_index,
token_modifiers_bitset, token_modifiers_bitset,
}; };
@ -963,8 +963,8 @@ impl LanguageServer for Backend {
semantic_tokens_options: SemanticTokensOptions { semantic_tokens_options: SemanticTokensOptions {
work_done_progress_options: WorkDoneProgressOptions::default(), work_done_progress_options: WorkDoneProgressOptions::default(),
legend: SemanticTokensLegend { legend: SemanticTokensLegend {
token_types: SEMANTIC_TOKEN_TYPES.to_vec(), token_types: SEMANTIC_TOKEN_TYPES.clone(),
token_modifiers: SEMANTIC_TOKEN_MODIFIERS.to_vec(), token_modifiers: SEMANTIC_TOKEN_MODIFIERS.clone(),
}, },
range: Some(false), range: Some(false),
full: Some(SemanticTokensFullOptions::Bool(true)), full: Some(SemanticTokensFullOptions::Bool(true)),

View File

@ -1082,7 +1082,7 @@ fn myFn = (param1) => {
// Get the token map. // Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(!token_map.is_empty()); assert!(token_map != vec![]);
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -2206,7 +2206,7 @@ part001 = cube([0,0], 20)
// Get the tokens. // Get the tokens.
let tokens = server.token_map.get("file:///test.kcl").unwrap().clone(); let tokens = server.token_map.get("file:///test.kcl").unwrap().clone();
assert_eq!(tokens.as_slice().len(), 120); assert_eq!(tokens.len(), 120);
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3379,11 +3379,11 @@ part001 = startSketchOn('XY')
// Get the symbols map. // Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(!symbols_map.is_empty()); assert!(symbols_map != vec![]);
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty()); assert!(semantic_tokens_map != vec![]);
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone(); let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
@ -3422,7 +3422,7 @@ NEW_LINT = 1"#
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty()); assert!(semantic_tokens_map != vec![]);
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl"); let memory = server.memory_map.get("file:///test.kcl");
@ -3466,7 +3466,7 @@ part001 = startSketchOn('XY')
// Get the token map. // Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(!token_map.is_empty()); assert!(token_map != vec![]);
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3474,11 +3474,11 @@ part001 = startSketchOn('XY')
// Get the symbols map. // Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(!symbols_map.is_empty()); assert!(symbols_map != vec![]);
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty()); assert!(semantic_tokens_map != vec![]);
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl").unwrap().clone(); let memory = server.memory_map.get("file:///test.kcl").unwrap().clone();
@ -3509,7 +3509,7 @@ part001 = startSketchOn('XY')
// Get the token map. // Get the token map.
let token_map = server.token_map.get("file:///test.kcl").unwrap().clone(); let token_map = server.token_map.get("file:///test.kcl").unwrap().clone();
assert!(!token_map.is_empty()); assert!(token_map != vec![]);
// Get the ast. // Get the ast.
let ast = server.ast_map.get("file:///test.kcl").unwrap().clone(); let ast = server.ast_map.get("file:///test.kcl").unwrap().clone();
@ -3517,11 +3517,11 @@ part001 = startSketchOn('XY')
// Get the symbols map. // Get the symbols map.
let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone(); let symbols_map = server.symbols_map.get("file:///test.kcl").unwrap().clone();
assert!(!symbols_map.is_empty()); assert!(symbols_map != vec![]);
// Get the semantic tokens map. // Get the semantic tokens map.
let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone(); let semantic_tokens_map = server.semantic_tokens_map.get("file:///test.kcl").unwrap().clone();
assert!(!semantic_tokens_map.is_empty()); assert!(semantic_tokens_map != vec![]);
// Get the memory. // Get the memory.
let memory = server.memory_map.get("file:///test.kcl"); let memory = server.memory_map.get("file:///test.kcl");

View File

@ -184,7 +184,7 @@ impl Node<Program> {
/// Walk the ast and get all the variables and tags as completion items. /// Walk the ast and get all the variables and tags as completion items.
pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> { pub fn completion_items<'a>(&'a self) -> Result<Vec<CompletionItem>> {
let completions = Arc::new(Mutex::new(vec![])); let completions = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, |node: crate::walk::Node<'a>| { crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?; let mut findings = completions.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
match node { match node {
crate::walk::Node::TagDeclarator(tag) => { crate::walk::Node::TagDeclarator(tag) => {
@ -195,7 +195,7 @@ impl Node<Program> {
} }
_ => {} _ => {}
} }
Ok::<bool, anyhow::Error>(true) Ok(true)
})?; })?;
let x = completions.lock().unwrap(); let x = completions.lock().unwrap();
Ok(x.clone()) Ok(x.clone())
@ -204,7 +204,7 @@ impl Node<Program> {
/// Returns all the lsp symbols in the program. /// Returns all the lsp symbols in the program.
pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> { pub fn get_lsp_symbols<'a>(&'a self, code: &str) -> Result<Vec<DocumentSymbol>> {
let symbols = Arc::new(Mutex::new(vec![])); let symbols = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, |node: crate::walk::Node<'a>| { crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
let mut findings = symbols.lock().map_err(|_| anyhow::anyhow!("mutex"))?; let mut findings = symbols.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
match node { match node {
crate::walk::Node::TagDeclarator(tag) => { crate::walk::Node::TagDeclarator(tag) => {
@ -215,7 +215,7 @@ impl Node<Program> {
} }
_ => {} _ => {}
} }
Ok::<bool, anyhow::Error>(true) Ok(true)
})?; })?;
let x = symbols.lock().unwrap(); let x = symbols.lock().unwrap();
Ok(x.clone()) Ok(x.clone())
@ -227,10 +227,10 @@ impl Node<Program> {
RuleT: crate::lint::Rule<'a>, RuleT: crate::lint::Rule<'a>,
{ {
let v = Arc::new(Mutex::new(vec![])); let v = Arc::new(Mutex::new(vec![]));
crate::walk::walk(self, |node: crate::walk::Node<'a>| { crate::walk::walk(self, &|node: crate::walk::Node<'a>| {
let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?; let mut findings = v.lock().map_err(|_| anyhow::anyhow!("mutex"))?;
findings.append(&mut rule.check(node)?); findings.append(&mut rule.check(node)?);
Ok::<bool, anyhow::Error>(true) Ok(true)
})?; })?;
let x = v.lock().unwrap(); let x = v.lock().unwrap();
Ok(x.clone()) Ok(x.clone())
@ -2810,8 +2810,7 @@ pub struct Parameter {
pub identifier: Node<Identifier>, pub identifier: Node<Identifier>,
/// The type of the parameter. /// The type of the parameter.
/// This is optional if the user defines a type. /// This is optional if the user defines a type.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(skip)]
#[ts(skip)]
pub type_: Option<FnArgType>, pub type_: Option<FnArgType>,
/// Is the parameter optional? /// Is the parameter optional?
/// If so, what is its default value? /// If so, what is its default value?

View File

@ -2,7 +2,7 @@ use crate::{
errors::{CompilationError, KclError, KclErrorDetails}, errors::{CompilationError, KclError, KclErrorDetails},
parsing::{ parsing::{
ast::types::{Node, Program}, ast::types::{Node, Program},
token::TokenStream, token::{Token, TokenType},
}, },
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
}; };
@ -34,13 +34,15 @@ pub fn top_level_parse(code: &str) -> ParseResult {
/// Parse the given KCL code into an AST. /// Parse the given KCL code into an AST.
pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult { pub fn parse_str(code: &str, module_id: ModuleId) -> ParseResult {
let tokens = pr_try!(crate::parsing::token::lex(code, module_id)); let tokens = pr_try!(crate::parsing::token::lexer(code, module_id));
parse_tokens(tokens) parse_tokens(tokens)
} }
/// Parse the supplied tokens into an AST. /// Parse the supplied tokens into an AST.
pub fn parse_tokens(mut tokens: TokenStream) -> ParseResult { pub fn parse_tokens(tokens: Vec<Token>) -> ParseResult {
let unknown_tokens = tokens.remove_unknown(); let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
.into_iter()
.partition(|token| token.token_type != TokenType::Unknown);
if !unknown_tokens.is_empty() { if !unknown_tokens.is_empty() {
let source_ranges = unknown_tokens.iter().map(SourceRange::from).collect(); let source_ranges = unknown_tokens.iter().map(SourceRange::from).collect();
@ -67,7 +69,7 @@ pub fn parse_tokens(mut tokens: TokenStream) -> ParseResult {
return Node::<Program>::default().into(); return Node::<Program>::default().into();
} }
parser::run_parser(tokens.as_slice()) parser::run_parser(&mut tokens.as_slice())
} }
/// Result of parsing. /// Result of parsing.

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +0,0 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 35,
"id": {
"end": 6,
"name": "foo",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 33,
"raw": "1",
"start": 32,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"end": 33,
"start": 25,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 35,
"start": 23
},
"end": 35,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 8,
"name": "x",
"start": 7,
"type": "Identifier"
},
"type_": {
"type": "Primitive",
"type": "Number"
},
"default_value": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"raw": "2"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 35,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 35,
"start": 0
}

View File

@ -1,72 +0,0 @@
---
source: kcl/src/parsing/parser.rs
expression: actual
snapshot_kind: text
---
{
"body": [
{
"declaration": {
"end": 27,
"id": {
"end": 6,
"name": "foo",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 25,
"raw": "1",
"start": 24,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"end": 25,
"start": 17,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 27,
"start": 15
},
"end": 27,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 8,
"name": "x",
"start": 7,
"type": "Identifier"
},
"default_value": {
"type": "Literal",
"type": "Literal",
"value": 2.0,
"raw": "2"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 27,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 27,
"start": 0
}

View File

@ -1,221 +1,28 @@
// Clippy does not agree with rustc here for some reason. use std::str::FromStr;
#![allow(clippy::needless_lifetimes)]
use std::{fmt, iter::Enumerate, num::NonZeroUsize};
use anyhow::Result; use anyhow::Result;
use parse_display::Display; use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tower_lsp::lsp_types::SemanticTokenType; use tower_lsp::lsp_types::SemanticTokenType;
use winnow::{ use winnow::{error::ParseError, stream::ContainsToken};
self,
error::ParseError,
stream::{ContainsToken, Stream},
};
use crate::{ use crate::{
errors::KclError, errors::KclError,
parsing::ast::types::{ItemVisibility, VariableKind}, parsing::ast::types::{ItemVisibility, VariableKind},
source_range::{ModuleId, SourceRange}, source_range::{ModuleId, SourceRange},
}; };
use tokeniser::Input;
mod tokeniser; mod tokeniser;
// Re-export
pub use tokeniser::Input;
#[cfg(test)] #[cfg(test)]
pub(crate) use tokeniser::RESERVED_WORDS; pub(crate) use tokeniser::RESERVED_WORDS;
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct TokenStream {
tokens: Vec<Token>,
}
impl TokenStream {
fn new(tokens: Vec<Token>) -> Self {
Self { tokens }
}
pub(super) fn remove_unknown(&mut self) -> Vec<Token> {
let tokens = std::mem::take(&mut self.tokens);
let (tokens, unknown_tokens): (Vec<Token>, Vec<Token>) = tokens
.into_iter()
.partition(|token| token.token_type != TokenType::Unknown);
self.tokens = tokens;
unknown_tokens
}
pub fn iter(&self) -> impl Iterator<Item = &Token> {
self.tokens.iter()
}
pub fn is_empty(&self) -> bool {
self.tokens.is_empty()
}
pub fn as_slice(&self) -> TokenSlice {
TokenSlice::from(self)
}
}
impl<'a> From<&'a TokenStream> for TokenSlice<'a> {
fn from(stream: &'a TokenStream) -> Self {
TokenSlice {
start: 0,
end: stream.tokens.len(),
stream,
}
}
}
impl IntoIterator for TokenStream {
type Item = Token;
type IntoIter = std::vec::IntoIter<Token>;
fn into_iter(self) -> Self::IntoIter {
self.tokens.into_iter()
}
}
#[derive(Debug, Clone)]
pub(crate) struct TokenSlice<'a> {
stream: &'a TokenStream,
start: usize,
end: usize,
}
impl<'a> std::ops::Deref for TokenSlice<'a> {
type Target = [Token];
fn deref(&self) -> &Self::Target {
&self.stream.tokens[self.start..self.end]
}
}
impl<'a> TokenSlice<'a> {
pub fn token(&self, i: usize) -> &Token {
&self.stream.tokens[i + self.start]
}
pub fn iter(&self) -> impl Iterator<Item = &Token> {
(**self).iter()
}
pub fn without_ends(&self) -> Self {
Self {
start: self.start + 1,
end: self.end - 1,
stream: self.stream,
}
}
}
impl<'a> IntoIterator for TokenSlice<'a> {
type Item = &'a Token;
type IntoIter = std::slice::Iter<'a, Token>;
fn into_iter(self) -> Self::IntoIter {
self.stream.tokens[self.start..self.end].iter()
}
}
impl<'a> Stream for TokenSlice<'a> {
type Token = Token;
type Slice = Self;
type IterOffsets = Enumerate<std::vec::IntoIter<Token>>;
type Checkpoint = Checkpoint;
fn iter_offsets(&self) -> Self::IterOffsets {
#[allow(clippy::unnecessary_to_owned)]
self.to_vec().into_iter().enumerate()
}
fn eof_offset(&self) -> usize {
self.len()
}
fn next_token(&mut self) -> Option<Self::Token> {
let token = self.first()?.clone();
self.start += 1;
Some(token)
}
fn offset_for<P>(&self, predicate: P) -> Option<usize>
where
P: Fn(Self::Token) -> bool,
{
self.iter().position(|b| predicate(b.clone()))
}
fn offset_at(&self, tokens: usize) -> Result<usize, winnow::error::Needed> {
if let Some(needed) = tokens.checked_sub(self.len()).and_then(NonZeroUsize::new) {
Err(winnow::error::Needed::Size(needed))
} else {
Ok(tokens)
}
}
fn next_slice(&mut self, offset: usize) -> Self::Slice {
assert!(self.start + offset <= self.end);
let next = TokenSlice {
stream: self.stream,
start: self.start,
end: self.start + offset,
};
self.start += offset;
next
}
fn checkpoint(&self) -> Self::Checkpoint {
Checkpoint(self.start, self.end)
}
fn reset(&mut self, checkpoint: &Self::Checkpoint) {
self.start = checkpoint.0;
self.end = checkpoint.1;
}
fn raw(&self) -> &dyn fmt::Debug {
self
}
}
impl<'a> winnow::stream::Offset for TokenSlice<'a> {
fn offset_from(&self, start: &Self) -> usize {
self.start - start.start
}
}
impl<'a> winnow::stream::Offset<Checkpoint> for TokenSlice<'a> {
fn offset_from(&self, start: &Checkpoint) -> usize {
self.start - start.0
}
}
impl winnow::stream::Offset for Checkpoint {
fn offset_from(&self, start: &Self) -> usize {
self.0 - start.0
}
}
impl<'a> winnow::stream::StreamIsPartial for TokenSlice<'a> {
type PartialState = ();
fn complete(&mut self) -> Self::PartialState {}
fn restore_partial(&mut self, _: Self::PartialState) {}
fn is_partial_supported() -> bool {
false
}
}
#[derive(Clone, Debug)]
pub struct Checkpoint(usize, usize);
/// The types of tokens. /// The types of tokens.
#[derive(Debug, PartialEq, Eq, Copy, Clone, Display)] #[derive(Debug, PartialEq, Eq, Copy, Clone, Deserialize, Serialize, JsonSchema, FromStr, Display)]
#[serde(rename_all = "camelCase")]
#[display(style = "camelCase")] #[display(style = "camelCase")]
pub enum TokenType { pub enum TokenType {
/// A number. /// A number.
@ -266,8 +73,6 @@ pub enum TokenType {
impl TryFrom<TokenType> for SemanticTokenType { impl TryFrom<TokenType> for SemanticTokenType {
type Error = anyhow::Error; type Error = anyhow::Error;
fn try_from(token_type: TokenType) -> Result<Self> { fn try_from(token_type: TokenType) -> Result<Self> {
// If you return a new kind of `SemanticTokenType`, make sure to update `SEMANTIC_TOKEN_TYPES`
// in the LSP implementation.
Ok(match token_type { Ok(match token_type {
TokenType::Number => Self::NUMBER, TokenType::Number => Self::NUMBER,
TokenType::Word => Self::VARIABLE, TokenType::Word => Self::VARIABLE,
@ -297,6 +102,52 @@ impl TryFrom<TokenType> for SemanticTokenType {
} }
impl TokenType { impl TokenType {
// This is for the lsp server.
// Don't call this function directly in the code use a lazy_static instead
// like we do in the lsp server.
pub fn all_semantic_token_types() -> Result<Vec<SemanticTokenType>> {
let mut settings = schemars::gen::SchemaSettings::openapi3();
settings.inline_subschemas = true;
let mut generator = schemars::gen::SchemaGenerator::new(settings);
let schema = TokenType::json_schema(&mut generator);
let schemars::schema::Schema::Object(o) = &schema else {
anyhow::bail!("expected object schema: {:#?}", schema);
};
let Some(subschemas) = &o.subschemas else {
anyhow::bail!("expected subschemas: {:#?}", schema);
};
let Some(one_ofs) = &subschemas.one_of else {
anyhow::bail!("expected one_of: {:#?}", schema);
};
let mut semantic_tokens = vec![];
for one_of in one_ofs {
let schemars::schema::Schema::Object(o) = one_of else {
anyhow::bail!("expected object one_of: {:#?}", one_of);
};
let Some(enum_values) = o.enum_values.as_ref() else {
anyhow::bail!("expected enum values: {:#?}", o);
};
if enum_values.len() > 1 {
anyhow::bail!("expected only one enum value: {:#?}", o);
}
if enum_values.is_empty() {
anyhow::bail!("expected at least one enum value: {:#?}", o);
}
let label = TokenType::from_str(&enum_values[0].to_string().replace('"', ""))?;
if let Ok(semantic_token_type) = SemanticTokenType::try_from(label) {
semantic_tokens.push(semantic_token_type);
}
}
Ok(semantic_tokens)
}
pub fn is_whitespace(&self) -> bool { pub fn is_whitespace(&self) -> bool {
matches!(self, Self::Whitespace) matches!(self, Self::Whitespace)
} }
@ -306,15 +157,17 @@ impl TokenType {
} }
} }
#[derive(Debug, PartialEq, Eq, Clone)] #[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Clone)]
pub struct Token { pub struct Token {
#[serde(rename = "type")]
pub token_type: TokenType, pub token_type: TokenType,
/// Offset in the source code where this token begins. /// Offset in the source code where this token begins.
pub start: usize, pub start: usize,
/// Offset in the source code where this token ends. /// Offset in the source code where this token ends.
pub end: usize, pub end: usize,
pub(super) module_id: ModuleId, #[serde(default, skip_serializing_if = "ModuleId::is_top_level")]
pub(super) value: String, pub module_id: ModuleId,
pub value: String,
} }
impl ContainsToken<Token> for (TokenType, &str) { impl ContainsToken<Token> for (TokenType, &str) {
@ -396,7 +249,7 @@ impl From<&Token> for SourceRange {
} }
} }
pub fn lex(s: &str, module_id: ModuleId) -> Result<TokenStream, KclError> { pub fn lexer(s: &str, module_id: ModuleId) -> Result<Vec<Token>, KclError> {
tokeniser::lex(s, module_id).map_err(From::from) tokeniser::lex(s, module_id).map_err(From::from)
} }
@ -428,3 +281,15 @@ impl From<ParseError<Input<'_>, winnow::error::ContextError>> for KclError {
}) })
} }
} }
#[cfg(test)]
mod tests {
use super::*;
// We have this as a test so we can ensure it never panics with an unwrap in the server.
#[test]
fn test_token_type_to_semantic_token_type() {
let semantic_types = TokenType::all_semantic_token_types().unwrap();
assert!(!semantic_types.is_empty());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -47,7 +47,7 @@ fn read(filename: &'static str, test_name: &str) -> String {
fn parse(test_name: &str) { fn parse(test_name: &str) {
let input = read("input.kcl", test_name); let input = read("input.kcl", test_name);
let tokens = crate::parsing::token::lex(&input, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lexer(&input, ModuleId::default()).unwrap();
// Parse the tokens into an AST. // Parse the tokens into an AST.
let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap()); let parse_res = Result::<_, KclError>::Ok(crate::parsing::parse_tokens(tokens).unwrap());
@ -1502,108 +1502,3 @@ mod kw_fn {
super::execute(TEST_NAME, true).await super::execute(TEST_NAME, true).await
} }
} }
mod tag_can_be_proxied_through_parameter {
const TEST_NAME: &str = "tag_can_be_proxied_through_parameter";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod tag_proxied_through_function_does_not_define_var {
const TEST_NAME: &str = "tag_proxied_through_function_does_not_define_var";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_too_few_args {
const TEST_NAME: &str = "kw_fn_too_few_args";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_unlabeled_but_has_label {
const TEST_NAME: &str = "kw_fn_unlabeled_but_has_label";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}
mod kw_fn_with_defaults {
const TEST_NAME: &str = "kw_fn_with_defaults";
/// Test parsing KCL.
#[test]
fn parse() {
super::parse(TEST_NAME)
}
/// Test that parsing and unparsing KCL produces the original KCL input.
#[test]
fn unparse() {
super::unparse(TEST_NAME)
}
/// Test that KCL is executed correctly.
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_execute() {
super::execute(TEST_NAME, false).await
}
}

View File

@ -50,13 +50,6 @@ pub struct KwArgs {
pub labeled: HashMap<String, Arg>, pub labeled: HashMap<String, Arg>,
} }
impl KwArgs {
/// How many arguments are there?
pub fn len(&self) -> usize {
self.labeled.len() + if self.unlabeled.is_some() { 1 } else { 0 }
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Args { pub struct Args {
/// Positional args. /// Positional args.

View File

@ -3,10 +3,10 @@ use std::fmt::Write;
use crate::parsing::{ use crate::parsing::{
ast::types::{ ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CallExpressionKw, DefaultParamVal, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression, CallExpressionKw, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression, ImportSelector,
ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue, MemberExpression,
MemberExpression, MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program, MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program, TagDeclarator,
TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind, UnaryExpression, VariableDeclaration, VariableKind,
}, },
PIPE_OPERATOR, PIPE_OPERATOR,
}; };
@ -166,14 +166,7 @@ pub(crate) enum ExprContext {
} }
impl Expr { impl Expr {
pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, mut ctxt: ExprContext) -> String { pub(crate) fn recast(&self, options: &FormatOptions, indentation_level: usize, ctxt: ExprContext) -> String {
let is_decl = matches!(ctxt, ExprContext::Decl);
if is_decl {
// Just because this expression is being bound to a variable, doesn't mean that every child
// expression is being bound. So, reset the expression context if necessary.
// This will still preserve the "::Pipe" context though.
ctxt = ExprContext::Other;
}
match &self { match &self {
Expr::BinaryExpression(bin_exp) => bin_exp.recast(options), Expr::BinaryExpression(bin_exp) => bin_exp.recast(options),
Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt), Expr::ArrayExpression(array_exp) => array_exp.recast(options, indentation_level, ctxt),
@ -182,7 +175,11 @@ impl Expr {
Expr::MemberExpression(mem_exp) => mem_exp.recast(), Expr::MemberExpression(mem_exp) => mem_exp.recast(),
Expr::Literal(literal) => literal.recast(), Expr::Literal(literal) => literal.recast(),
Expr::FunctionExpression(func_exp) => { Expr::FunctionExpression(func_exp) => {
let mut result = if is_decl { String::new() } else { "fn".to_owned() }; let mut result = if ctxt == ExprContext::Decl {
String::new()
} else {
"fn".to_owned()
};
result += &func_exp.recast(options, indentation_level); result += &func_exp.recast(options, indentation_level);
result result
} }
@ -662,18 +659,15 @@ impl FunctionExpression {
impl Parameter { impl Parameter {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String { pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let at_sign = if self.labeled { "" } else { "@" }; let mut result = format!(
let identifier = &self.identifier.name; "{}{}",
let question_mark = if self.default_value.is_some() { "?" } else { "" }; if self.labeled { "" } else { "@" },
let mut result = format!("{at_sign}{identifier}{question_mark}"); self.identifier.name.clone()
);
if let Some(ty) = &self.type_ { if let Some(ty) = &self.type_ {
result += ": "; result += ": ";
result += &ty.recast(options, indentation_level); result += &ty.recast(options, indentation_level);
} }
if let Some(DefaultParamVal::Literal(ref literal)) = self.default_value {
let lit = literal.recast();
result.push_str(&format!(" = {lit}"));
};
result result
} }
@ -2140,10 +2134,8 @@ fn f() {
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
let tokens = crate::parsing::token::lex(raw, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lexer(raw, ModuleId::default()).unwrap();
let literal = crate::parsing::parser::unsigned_number_literal let literal = crate::parsing::parser::unsigned_number_literal.parse(&tokens).unwrap();
.parse(tokens.as_slice())
.unwrap();
assert_eq!( assert_eq!(
literal.recast(), literal.recast(),
expected, expected,
@ -2178,28 +2170,6 @@ sketch002 = startSketchOn({
assert_eq!(actual, expected); assert_eq!(actual, expected);
} }
#[test]
fn unparse_fn_unnamed() {
let input = r#"squares_out = reduce(arr, 0, fn(i, squares) {
return 1
})
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, input);
}
#[test]
fn unparse_fn_named() {
let input = r#"fn f(x) {
return 1
}
"#;
let ast = crate::parsing::top_level_parse(input).unwrap();
let actual = ast.recast(&FormatOptions::new(), 0);
assert_eq!(actual, input);
}
#[test] #[test]
fn recast_objects_with_comments() { fn recast_objects_with_comments() {
use winnow::Parser; use winnow::Parser;
@ -2221,9 +2191,9 @@ sketch002 = startSketchOn({
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap();
crate::parsing::parser::print_tokens(tokens.as_slice()); crate::parsing::parser::print_tokens(&tokens);
let expr = crate::parsing::parser::object.parse(tokens.as_slice()).unwrap(); let expr = crate::parsing::parser::object.parse(&tokens).unwrap();
assert_eq!( assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other), expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected, expected,
@ -2319,10 +2289,8 @@ sketch002 = startSketchOn({
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
let tokens = crate::parsing::token::lex(input, ModuleId::default()).unwrap(); let tokens = crate::parsing::token::lexer(input, ModuleId::default()).unwrap();
let expr = crate::parsing::parser::array_elem_by_elem let expr = crate::parsing::parser::array_elem_by_elem.parse(&tokens).unwrap();
.parse(tokens.as_slice())
.unwrap();
assert_eq!( assert_eq!(
expr.recast(&FormatOptions::new(), 0, ExprContext::Other), expr.recast(&FormatOptions::new(), 0, ExprContext::Other),
expected, expected,

View File

@ -5,7 +5,7 @@ use crate::{
/// The "Node" type wraps all the AST elements we're able to find in a KCL /// The "Node" type wraps all the AST elements we're able to find in a KCL
/// file. Tokens we walk through will be one of these. /// file. Tokens we walk through will be one of these.
#[derive(Copy, Clone, Debug)] #[derive(Clone, Debug)]
pub enum Node<'a> { pub enum Node<'a> {
Program(NodeRef<'a, types::Program>), Program(NodeRef<'a, types::Program>),
@ -31,7 +31,6 @@ pub enum Node<'a> {
MemberExpression(NodeRef<'a, types::MemberExpression>), MemberExpression(NodeRef<'a, types::MemberExpression>),
UnaryExpression(NodeRef<'a, types::UnaryExpression>), UnaryExpression(NodeRef<'a, types::UnaryExpression>),
IfExpression(NodeRef<'a, types::IfExpression>), IfExpression(NodeRef<'a, types::IfExpression>),
ElseIf(&'a types::ElseIf),
Parameter(&'a types::Parameter), Parameter(&'a types::Parameter),
@ -39,22 +38,11 @@ pub enum Node<'a> {
MemberObject(&'a types::MemberObject), MemberObject(&'a types::MemberObject),
LiteralIdentifier(&'a types::LiteralIdentifier), LiteralIdentifier(&'a types::LiteralIdentifier),
KclNone(&'a types::KclNone),
} }
/// Returned during source_range conversion. impl From<&Node<'_>> for SourceRange {
#[derive(Debug)] fn from(node: &Node) -> Self {
pub enum AstNodeError { match node {
/// Returned if we try and [SourceRange] a [types::KclNone].
NoSourceForAKclNone,
}
impl TryFrom<&Node<'_>> for SourceRange {
type Error = AstNodeError;
fn try_from(node: &Node) -> Result<Self, Self::Error> {
Ok(match node {
Node::Program(n) => SourceRange::from(*n), Node::Program(n) => SourceRange::from(*n),
Node::ImportStatement(n) => SourceRange::from(*n), Node::ImportStatement(n) => SourceRange::from(*n),
Node::ExpressionStatement(n) => SourceRange::from(*n), Node::ExpressionStatement(n) => SourceRange::from(*n),
@ -80,62 +68,6 @@ impl TryFrom<&Node<'_>> for SourceRange {
Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()), Node::MemberObject(m) => SourceRange::new(m.start(), m.end(), m.module_id()),
Node::IfExpression(n) => SourceRange::from(*n), Node::IfExpression(n) => SourceRange::from(*n),
Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()), Node::LiteralIdentifier(l) => SourceRange::new(l.start(), l.end(), l.module_id()),
// This is broken too
Node::ElseIf(n) => SourceRange::new(n.cond.start(), n.cond.end(), n.cond.module_id()),
// The KclNone type here isn't an actual node, so it has no
// start/end information.
Node::KclNone(_) => return Err(Self::Error::NoSourceForAKclNone),
})
}
}
impl<'tree> From<&'tree types::BodyItem> for Node<'tree> {
fn from(node: &'tree types::BodyItem) -> Self {
match node {
types::BodyItem::ImportStatement(v) => v.as_ref().into(),
types::BodyItem::ExpressionStatement(v) => v.into(),
types::BodyItem::VariableDeclaration(v) => v.as_ref().into(),
types::BodyItem::ReturnStatement(v) => v.into(),
}
}
}
impl<'tree> From<&'tree types::Expr> for Node<'tree> {
fn from(node: &'tree types::Expr) -> Self {
match node {
types::Expr::Literal(lit) => lit.as_ref().into(),
types::Expr::TagDeclarator(tag) => tag.as_ref().into(),
types::Expr::Identifier(id) => id.as_ref().into(),
types::Expr::BinaryExpression(be) => be.as_ref().into(),
types::Expr::FunctionExpression(fe) => fe.as_ref().into(),
types::Expr::CallExpression(ce) => ce.as_ref().into(),
types::Expr::CallExpressionKw(ce) => ce.as_ref().into(),
types::Expr::PipeExpression(pe) => pe.as_ref().into(),
types::Expr::PipeSubstitution(ps) => ps.as_ref().into(),
types::Expr::ArrayExpression(ae) => ae.as_ref().into(),
types::Expr::ArrayRangeExpression(are) => are.as_ref().into(),
types::Expr::ObjectExpression(oe) => oe.as_ref().into(),
types::Expr::MemberExpression(me) => me.as_ref().into(),
types::Expr::UnaryExpression(ue) => ue.as_ref().into(),
types::Expr::IfExpression(e) => e.as_ref().into(),
types::Expr::None(n) => n.into(),
}
}
}
impl<'tree> From<&'tree types::BinaryPart> for Node<'tree> {
fn from(node: &'tree types::BinaryPart) -> Self {
match node {
types::BinaryPart::Literal(lit) => lit.as_ref().into(),
types::BinaryPart::Identifier(id) => id.as_ref().into(),
types::BinaryPart::BinaryExpression(be) => be.as_ref().into(),
types::BinaryPart::CallExpression(ce) => ce.as_ref().into(),
types::BinaryPart::CallExpressionKw(ce) => ce.as_ref().into(),
types::BinaryPart::UnaryExpression(ue) => ue.as_ref().into(),
types::BinaryPart::MemberExpression(me) => me.as_ref().into(),
types::BinaryPart::IfExpression(e) => e.as_ref().into(),
} }
} }
} }
@ -184,6 +116,4 @@ impl_from!(Node, ObjectProperty);
impl_from_ref!(Node, Parameter); impl_from_ref!(Node, Parameter);
impl_from_ref!(Node, MemberObject); impl_from_ref!(Node, MemberObject);
impl_from!(Node, IfExpression); impl_from!(Node, IfExpression);
impl_from!(Node, ElseIf);
impl_from_ref!(Node, LiteralIdentifier); impl_from_ref!(Node, LiteralIdentifier);
impl_from!(Node, KclNone);

View File

@ -1,197 +0,0 @@
use anyhow::Result;
use crate::walk::Node;
/// Walk-specific trait adding the ability to traverse the KCL AST.
///
/// This trait is implemented on [Node] to handle the fairly tricky bit of
/// recursing into the AST in a single place, as well as helpers for traversing
/// the tree. for callers to use.
pub trait Visitable<'tree> {
/// Return a `Vec<Node>` for all *direct* children of this AST node. This
/// should only contain direct descendants.
fn children(&self) -> Vec<Node<'tree>>;
/// Return `self` as a [Node]. Generally speaking, the [Visitable] trait
/// is only going to be implemented on [Node], so this is purely used by
/// helpers that are generic over a [Visitable] and want to deref back
/// into a [Node].
fn node(&self) -> Node<'tree>;
/// Call the provided [Visitor] in order to Visit `self`. This will
/// only be called on `self` -- the [Visitor] is responsible for
/// recursing into any children, if desired.
fn visit<VisitorT>(&self, visitor: VisitorT) -> Result<bool, VisitorT::Error>
where
VisitorT: Visitor<'tree>,
{
visitor.visit_node(self.node())
}
}
/// Trait used to enable visiting members of KCL AST.
///
/// Implementing this trait enables the implementer to be invoked over
/// members of KCL AST by using the [Visitable::visit] function on
/// a [Node].
pub trait Visitor<'tree> {
/// Error type returned by the [Self::visit] function.
type Error;
/// Visit a KCL AST [Node].
///
/// In general, implementers likely wish to check to see if a Node is what
/// they're looking for, and either descend into that [Node]'s children (by
/// calling [Visitable::children] on [Node] to get children nodes,
/// calling [Visitable::visit] on each node of interest), or perform
/// some action.
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error>;
}
impl<'a, FnT, ErrorT> Visitor<'a> for FnT
where
FnT: Fn(Node<'a>) -> Result<bool, ErrorT>,
{
type Error = ErrorT;
fn visit_node(&self, n: Node<'a>) -> Result<bool, ErrorT> {
self(n)
}
}
impl<'tree> Visitable<'tree> for Node<'tree> {
fn node(&self) -> Node<'tree> {
*self
}
fn children(&self) -> Vec<Node<'tree>> {
match self {
Node::Program(n) => n.body.iter().map(|node| node.into()).collect(),
Node::ExpressionStatement(n) => {
vec![(&n.expression).into()]
}
Node::BinaryExpression(n) => {
vec![(&n.left).into(), (&n.right).into()]
}
Node::FunctionExpression(n) => {
let mut children = n.params.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.push((&n.body).into());
children
}
Node::CallExpression(n) => {
let mut children = n.arguments.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.insert(0, (&n.callee).into());
children
}
Node::CallExpressionKw(n) => {
let mut children = n.unlabeled.iter().map(|v| v.into()).collect::<Vec<Node>>();
// TODO: this is wrong but it's what the old walk code was doing.
// We likely need a real LabeledArg AST node, but I don't
// want to tango with it since it's a lot deeper than
// adding it to the enum.
children.extend(n.arguments.iter().map(|v| (&v.arg).into()).collect::<Vec<Node>>());
children
}
Node::PipeExpression(n) => n.body.iter().map(|v| v.into()).collect(),
Node::ArrayExpression(n) => n.elements.iter().map(|v| v.into()).collect(),
Node::ArrayRangeExpression(n) => {
vec![(&n.start_element).into(), (&n.end_element).into()]
}
Node::ObjectExpression(n) => n.properties.iter().map(|v| v.into()).collect(),
Node::MemberExpression(n) => {
vec![(&n.object).into(), (&n.property).into()]
}
Node::IfExpression(n) => {
let mut children = n.else_ifs.iter().map(|v| v.into()).collect::<Vec<Node>>();
children.insert(0, n.cond.as_ref().into());
children.push(n.final_else.as_ref().into());
children
}
Node::VariableDeclaration(n) => vec![(&n.declaration).into()],
Node::ReturnStatement(n) => {
vec![(&n.argument).into()]
}
Node::VariableDeclarator(n) => {
vec![(&n.id).into(), (&n.init).into()]
}
Node::UnaryExpression(n) => {
vec![(&n.argument).into()]
}
Node::Parameter(n) => {
vec![(&n.identifier).into()]
}
Node::ObjectProperty(n) => {
vec![(&n.value).into()]
}
Node::ElseIf(n) => {
vec![(&n.cond).into(), n.then_val.as_ref().into()]
}
Node::PipeSubstitution(_)
| Node::TagDeclarator(_)
| Node::Identifier(_)
| Node::ImportStatement(_)
| Node::MemberObject(_)
| Node::LiteralIdentifier(_)
| Node::KclNone(_)
| Node::Literal(_) => vec![],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
macro_rules! kcl {
( $kcl:expr ) => {{
$crate::parsing::top_level_parse($kcl).unwrap()
}};
}
#[test]
fn count_crows() {
let program = kcl!(
"\
const crow1 = 1
const crow2 = 2
fn crow3() {
const crow4 = 3
crow5()
}
"
);
#[derive(Debug, Default)]
struct CountCrows {
n: Box<Mutex<usize>>,
}
impl<'tree> Visitor<'tree> for &CountCrows {
type Error = ();
fn visit_node(&self, node: Node<'tree>) -> Result<bool, Self::Error> {
if let Node::VariableDeclarator(vd) = node {
if vd.id.name.starts_with("crow") {
*self.n.lock().unwrap() += 1;
}
}
for child in node.children().iter() {
if !child.visit(*self)? {
return Ok(false);
}
}
Ok(true)
}
}
let prog: Node = (&program).into();
let count_crows: CountCrows = Default::default();
Visitable::visit(&prog, &count_crows).unwrap();
assert_eq!(*count_crows.n.lock().unwrap(), 4);
}
}

View File

@ -1,55 +1,329 @@
use anyhow::Result; use anyhow::Result;
use super::ast_visitor::{Visitable, Visitor};
use crate::{ use crate::{
parsing::ast::types::{NodeRef, Program}, parsing::ast::types::{
BinaryPart, BodyItem, Expr, IfExpression, LiteralIdentifier, MemberExpression, MemberObject, NodeRef,
ObjectExpression, ObjectProperty, Parameter, Program, UnaryExpression, VariableDeclarator,
},
walk::Node, walk::Node,
}; };
/// *DEPRECATED* Walk trait. /// Walker is implemented by things that are able to walk an AST tree to
/// /// produce lints. This trait is implemented automatically for a few of the
/// This was written before [Visitor], which is the better way to traverse /// common types, but can be manually implemented too.
/// a AST.
///
/// This trait continues to exist in order to not change all the linter
/// as we refine the walk code.
///
/// This, internally, uses the new [Visitor] trait, and is only provided as
/// a stub until we migrate all existing code off this trait.
pub trait Walker<'a> { pub trait Walker<'a> {
/// Walk will visit every element of the AST, recursing through the /// Walk will visit every element of the AST.
/// whole tree.
fn walk(&self, n: Node<'a>) -> Result<bool>; fn walk(&self, n: Node<'a>) -> Result<bool>;
} }
impl<'tree, VisitorT> Walker<'tree> for VisitorT impl<'a, FnT> Walker<'a> for FnT
where where
VisitorT: Visitor<'tree>, FnT: Fn(Node<'a>) -> Result<bool>,
VisitorT: Clone,
anyhow::Error: From<VisitorT::Error>,
VisitorT::Error: Send,
VisitorT::Error: Sync,
{ {
fn walk(&self, n: Node<'tree>) -> Result<bool> { fn walk(&self, n: Node<'a>) -> Result<bool> {
if !n.visit(self.clone())? { self(n)
return Ok(false);
}
for child in n.children() {
if !Self::walk(self, child)? {
return Ok(false);
}
}
Ok(true)
} }
} }
/// Run the Walker against all [Node]s in a [Program]. /// Run the Walker against all [Node]s in a [Program].
pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: WalkT) -> Result<bool> pub fn walk<'a, WalkT>(prog: NodeRef<'a, Program>, f: &WalkT) -> Result<bool>
where where
WalkT: Walker<'a>, WalkT: Walker<'a>,
{ {
let prog: Node = prog.into(); if !f.walk(prog.into())? {
f.walk(prog) return Ok(false);
}
for bi in &prog.body {
if !walk_body_item(bi, f)? {
return Ok(false);
}
}
Ok(true)
}
fn walk_variable_declarator<'a, WalkT>(node: NodeRef<'a, VariableDeclarator>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !f.walk((&node.id).into())? {
return Ok(false);
}
walk_value(&node.init, f)
}
fn walk_parameter<'a, WalkT>(node: &'a Parameter, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
f.walk((&node.identifier).into())
}
fn walk_member_object<'a, WalkT>(node: &'a MemberObject, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
f.walk(node.into())
}
fn walk_literal_identifier<'a, WalkT>(node: &'a LiteralIdentifier, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
f.walk(node.into())
}
fn walk_member_expression<'a, WalkT>(node: NodeRef<'a, MemberExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !walk_member_object(&node.object, f)? {
return Ok(false);
}
walk_literal_identifier(&node.property, f)
}
fn walk_binary_part<'a, WalkT>(node: &'a BinaryPart, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
match node {
BinaryPart::Literal(lit) => f.walk(lit.as_ref().into()),
BinaryPart::Identifier(id) => f.walk(id.as_ref().into()),
BinaryPart::BinaryExpression(be) => f.walk(be.as_ref().into()),
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into()),
BinaryPart::CallExpressionKw(ce) => f.walk(ce.as_ref().into()),
BinaryPart::UnaryExpression(ue) => walk_unary_expression(ue, f),
BinaryPart::MemberExpression(me) => walk_member_expression(me, f),
BinaryPart::IfExpression(e) => walk_if_expression(e, f),
}
}
// TODO: Rename this to walk_expr
fn walk_value<'a, WalkT>(node: &'a Expr, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
match node {
Expr::Literal(lit) => f.walk(lit.as_ref().into()),
Expr::TagDeclarator(tag) => f.walk(tag.as_ref().into()),
Expr::Identifier(id) => {
// sometimes there's a bare Identifier without a Value::Identifier.
f.walk(id.as_ref().into())
}
Expr::BinaryExpression(be) => {
if !f.walk(be.as_ref().into())? {
return Ok(false);
}
if !walk_binary_part(&be.left, f)? {
return Ok(false);
}
walk_binary_part(&be.right, f)
}
Expr::FunctionExpression(fe) => {
if !f.walk(fe.as_ref().into())? {
return Ok(false);
}
for arg in &fe.params {
if !walk_parameter(arg, f)? {
return Ok(false);
}
}
walk(&fe.body, f)
}
Expr::CallExpression(ce) => {
if !f.walk(ce.as_ref().into())? {
return Ok(false);
}
if !f.walk((&ce.callee).into())? {
return Ok(false);
}
for e in &ce.arguments {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::CallExpressionKw(ce) => {
if !f.walk(ce.as_ref().into())? {
return Ok(false);
}
if !f.walk((&ce.callee).into())? {
return Ok(false);
}
if let Some(ref e) = ce.unlabeled {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
for e in &ce.arguments {
if !walk_value::<WalkT>(&e.arg, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::PipeExpression(pe) => {
if !f.walk(pe.as_ref().into())? {
return Ok(false);
}
for e in &pe.body {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::PipeSubstitution(ps) => f.walk(ps.as_ref().into()),
Expr::ArrayExpression(ae) => {
if !f.walk(ae.as_ref().into())? {
return Ok(false);
}
for e in &ae.elements {
if !walk_value::<WalkT>(e, f)? {
return Ok(false);
}
}
Ok(true)
}
Expr::ArrayRangeExpression(are) => {
if !f.walk(are.as_ref().into())? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.start_element, f)? {
return Ok(false);
}
if !walk_value::<WalkT>(&are.end_element, f)? {
return Ok(false);
}
Ok(true)
}
Expr::ObjectExpression(oe) => walk_object_expression(oe, f),
Expr::MemberExpression(me) => walk_member_expression(me, f),
Expr::UnaryExpression(ue) => walk_unary_expression(ue, f),
Expr::IfExpression(e) => walk_if_expression(e, f),
Expr::None(_) => Ok(true),
}
}
/// Walk through an [ObjectProperty].
fn walk_object_property<'a, WalkT>(node: NodeRef<'a, ObjectProperty>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
walk_value(&node.value, f)
}
/// Walk through an [ObjectExpression].
fn walk_object_expression<'a, WalkT>(node: NodeRef<'a, ObjectExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
for prop in &node.properties {
if !walk_object_property(prop, f)? {
return Ok(false);
}
}
Ok(true)
}
/// Walk through an [IfExpression].
fn walk_if_expression<'a, WalkT>(node: NodeRef<'a, IfExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !walk_value(&node.cond, f)? {
return Ok(false);
}
for else_if in &node.else_ifs {
if !walk_value(&else_if.cond, f)? {
return Ok(false);
}
if !walk(&else_if.then_val, f)? {
return Ok(false);
}
}
let final_else = &(*node.final_else);
if !f.walk(final_else.into())? {
return Ok(false);
}
Ok(true)
}
/// walk through an [UnaryExpression].
fn walk_unary_expression<'a, WalkT>(node: NodeRef<'a, UnaryExpression>, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
walk_binary_part(&node.argument, f)
}
/// walk through a [BodyItem].
fn walk_body_item<'a, WalkT>(node: &'a BodyItem, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
// We don't walk a BodyItem since it's an enum itself.
match node {
BodyItem::ImportStatement(xs) => {
if !f.walk(xs.as_ref().into())? {
return Ok(false);
}
Ok(true)
}
BodyItem::ExpressionStatement(xs) => {
if !f.walk(xs.into())? {
return Ok(false);
}
walk_value(&xs.expression, f)
}
BodyItem::VariableDeclaration(vd) => {
if !f.walk(vd.as_ref().into())? {
return Ok(false);
}
walk_variable_declarator(&vd.declaration, f)
}
BodyItem::ReturnStatement(rs) => {
if !f.walk(rs.into())? {
return Ok(false);
}
walk_value(&rs.argument, f)
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -71,10 +345,10 @@ const bar = 2
" "
); );
walk(&program, |node| { walk(&program, &|node| {
if let Node::VariableDeclarator(vd) = node { if let Node::VariableDeclarator(vd) = node {
if vd.id.name == "foo" { if vd.id.name == "foo" {
return Ok::<bool, anyhow::Error>(false); return Ok(false);
} }
panic!("walk didn't stop"); panic!("walk didn't stop");
} }

View File

@ -1,5 +1,4 @@
mod ast_node; mod ast_node;
mod ast_visitor;
mod ast_walk; mod ast_walk;
pub use ast_node::Node; pub use ast_node::Node;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 127 KiB

View File

@ -78,179 +78,46 @@ snapshot_kind: text
}, },
{ {
"declaration": { "declaration": {
"end": 77, "end": 55,
"id": { "id": {
"end": 43, "end": 40,
"name": "add",
"start": 40,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 75,
"left": {
"end": 67,
"name": "x",
"start": 66,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 75,
"name": "delta",
"start": 70,
"type": "Identifier",
"type": "Identifier"
},
"start": 66,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 75,
"start": 59,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 77,
"start": 55
},
"end": 77,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 46,
"name": "x",
"start": 45,
"type": "Identifier"
},
"labeled": false
},
{
"type": "Parameter",
"identifier": {
"end": 53,
"name": "delta",
"start": 48,
"type": "Identifier"
}
}
],
"start": 43,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 40,
"type": "VariableDeclarator"
},
"end": 77,
"kind": "fn",
"start": 37,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 97,
"id": {
"end": 82,
"name": "two", "name": "two",
"start": 79, "start": 37,
"type": "Identifier" "type": "Identifier"
}, },
"init": { "init": {
"arguments": [ "arguments": [
{ {
"end": 96, "end": 54,
"raw": "1", "raw": "1",
"start": 95, "start": 53,
"type": "Literal", "type": "Literal",
"type": "Literal", "type": "Literal",
"value": 1.0 "value": 1.0
} }
], ],
"callee": { "callee": {
"end": 94, "end": 52,
"name": "increment", "name": "increment",
"start": 85, "start": 43,
"type": "Identifier" "type": "Identifier"
}, },
"end": 97, "end": 55,
"start": 85, "start": 43,
"type": "CallExpression", "type": "CallExpression",
"type": "CallExpression" "type": "CallExpression"
}, },
"start": 79, "start": 37,
"type": "VariableDeclarator" "type": "VariableDeclarator"
}, },
"end": 97, "end": 55,
"kind": "const", "kind": "const",
"start": 79, "start": 37,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 122,
"id": {
"end": 103,
"name": "three",
"start": 98,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "delta"
},
"arg": {
"end": 121,
"raw": "2",
"start": 120,
"type": "Literal",
"type": "Literal",
"value": 2.0
}
}
],
"callee": {
"end": 109,
"name": "add",
"start": 106,
"type": "Identifier"
},
"end": 122,
"start": 106,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"end": 111,
"raw": "1",
"start": 110,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
},
"start": 98,
"type": "VariableDeclarator"
},
"end": 122,
"kind": "const",
"start": 98,
"type": "VariableDeclaration", "type": "VariableDeclaration",
"type": "VariableDeclaration" "type": "VariableDeclaration"
} }
], ],
"end": 123, "end": 56,
"nonCodeMeta": { "nonCodeMeta": {
"nonCodeNodes": { "nonCodeNodes": {
"0": [ "0": [
@ -262,16 +129,6 @@ snapshot_kind: text
"type": "newLine" "type": "newLine"
} }
} }
],
"1": [
{
"end": 79,
"start": 77,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
] ]
}, },
"startNodes": [] "startNodes": []

View File

@ -2,9 +2,4 @@ fn increment(@x) {
return x + 1 return x + 1
} }
fn add(@x, delta) {
return x + delta
}
two = increment(1) two = increment(1)
three = add(1, delta: 2)

View File

@ -27,202 +27,6 @@ snapshot_kind: text
"value": 0.0, "value": 0.0,
"__meta": [] "__meta": []
}, },
"add": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"end": 75,
"left": {
"end": 67,
"name": "x",
"start": 66,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 75,
"name": "delta",
"start": 70,
"type": "Identifier",
"type": "Identifier"
},
"start": 66,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 75,
"start": 59,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 77,
"start": 55
},
"end": 77,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 46,
"name": "x",
"start": 45,
"type": "Identifier"
},
"labeled": false
},
{
"type": "Parameter",
"identifier": {
"end": 53,
"name": "delta",
"start": 48,
"type": "Identifier"
}
}
],
"start": 43,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
},
"increment": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"end": 33,
"left": {
"end": 29,
"name": "x",
"start": 28,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 33,
"raw": "1",
"start": 32,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"start": 28,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 33,
"start": 21,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 35,
"start": 17
},
"end": 35,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 15,
"name": "x",
"start": 14,
"type": "Identifier"
},
"labeled": false
}
],
"start": 12,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
12,
35,
0
]
}
]
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
43,
77,
0
]
}
]
},
"increment": { "increment": {
"type": "Function", "type": "Function",
"expression": { "expression": {
@ -317,34 +121,14 @@ snapshot_kind: text
} }
] ]
}, },
"three": {
"type": "Number",
"value": 3.0,
"__meta": [
{
"sourceRange": [
110,
111,
0
]
},
{
"sourceRange": [
120,
121,
0
]
}
]
},
"two": { "two": {
"type": "Number", "type": "Number",
"value": 2.0, "value": 2.0,
"__meta": [ "__meta": [
{ {
"sourceRange": [ "sourceRange": [
95, 53,
96, 54,
0 0
] ]
}, },

View File

@ -1,153 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing kw_fn_too_few_args.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 31,
"id": {
"end": 6,
"name": "add",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 29,
"left": {
"end": 25,
"name": "x",
"start": 24,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 29,
"name": "y",
"start": 28,
"type": "Identifier",
"type": "Identifier"
},
"start": 24,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 29,
"start": 17,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 31,
"start": 13
},
"end": 31,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 8,
"name": "x",
"start": 7,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 11,
"name": "y",
"start": 10,
"type": "Identifier"
}
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 31,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 50,
"id": {
"end": 38,
"name": "three",
"start": 33,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "x"
},
"arg": {
"end": 49,
"raw": "1",
"start": 48,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
}
],
"callee": {
"end": 44,
"name": "add",
"start": 41,
"type": "Identifier"
},
"end": 50,
"start": 41,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 33,
"type": "VariableDeclarator"
},
"end": 50,
"kind": "const",
"start": 33,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 51,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 33,
"start": 31,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -1,17 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing kw_fn_too_few_args.kcl
snapshot_kind: text
---
KCL Semantic error
× semantic: This function requires a parameter y, but you haven't passed
│ it one.
╭─[1:7]
1 │ ╭─▶ fn add(x, y) {
2 │ │ return x + y
3 │ ╰─▶ }
4 │
5 │ three = add(x: 1)
· ─────────
╰────

View File

@ -1,5 +0,0 @@
fn add(x, y) {
return x + y
}
three = add(x: 1)

View File

@ -1,146 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing kw_fn_unlabeled_but_has_label.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 29,
"id": {
"end": 6,
"name": "add",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 27,
"left": {
"end": 23,
"name": "x",
"start": 22,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 27,
"raw": "1",
"start": 26,
"type": "Literal",
"type": "Literal",
"value": 1.0
},
"start": 22,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 27,
"start": 15,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 29,
"start": 11
},
"end": 29,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 9,
"name": "x",
"start": 8,
"type": "Identifier"
},
"labeled": false
}
],
"start": 6,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 29,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 46,
"id": {
"end": 34,
"name": "two",
"start": 31,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "x"
},
"arg": {
"end": 45,
"raw": "1",
"start": 44,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
}
],
"callee": {
"end": 40,
"name": "add",
"start": 37,
"type": "Identifier"
},
"end": 46,
"start": 37,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": null
},
"start": 31,
"type": "VariableDeclarator"
},
"end": 46,
"kind": "const",
"start": 31,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 47,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 31,
"start": 29,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -1,17 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing kw_fn_unlabeled_but_has_label.kcl
snapshot_kind: text
---
KCL Semantic error
× semantic: The function does declare a parameter named 'x', but this
│ parameter doesn't use a label. Try removing the `x:`
╭─[1:7]
1 │ ╭─▶ fn add(@x) {
2 │ │ return x + 1
3 │ ╰─▶ }
4 │
5 │ two = add(x: 1)
· ─────────
╰────

View File

@ -1,5 +0,0 @@
fn add(@x) {
return x + 1
}
two = add(x: 1)

View File

@ -1,207 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing kw_fn_with_defaults.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 45,
"id": {
"end": 12,
"name": "increment",
"start": 3,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"end": 43,
"left": {
"end": 38,
"name": "x",
"start": 37,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 43,
"name": "by",
"start": 41,
"type": "Identifier",
"type": "Identifier"
},
"start": 37,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 43,
"start": 30,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 45,
"start": 26
},
"end": 45,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 15,
"name": "x",
"start": 14,
"type": "Identifier"
},
"labeled": false
},
{
"type": "Parameter",
"identifier": {
"end": 19,
"name": "by",
"start": 17,
"type": "Identifier"
},
"default_value": {
"type": "Literal",
"type": "Literal",
"value": 1.0,
"raw": "1"
}
}
],
"start": 12,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 3,
"type": "VariableDeclarator"
},
"end": 45,
"kind": "fn",
"start": 0,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 65,
"id": {
"end": 50,
"name": "two",
"start": 47,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 64,
"raw": "1",
"start": 63,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
],
"callee": {
"end": 62,
"name": "increment",
"start": 53,
"type": "Identifier"
},
"end": 65,
"start": 53,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 47,
"type": "VariableDeclarator"
},
"end": 65,
"kind": "const",
"start": 47,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 98,
"id": {
"end": 75,
"name": "twentyOne",
"start": 66,
"type": "Identifier"
},
"init": {
"arguments": [
{
"type": "LabeledArg",
"label": {
"type": "Identifier",
"name": "by"
},
"arg": {
"end": 97,
"raw": "20",
"start": 95,
"type": "Literal",
"type": "Literal",
"value": 20.0
}
}
],
"callee": {
"end": 87,
"name": "increment",
"start": 78,
"type": "Identifier"
},
"end": 98,
"start": 78,
"type": "CallExpressionKw",
"type": "CallExpressionKw",
"unlabeled": {
"end": 89,
"raw": "1",
"start": 88,
"type": "Literal",
"type": "Literal",
"value": 1.0
}
},
"start": 66,
"type": "VariableDeclarator"
},
"end": 98,
"kind": "const",
"start": 66,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 99,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 47,
"start": 45,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": []
},
"start": 0
}
}

View File

@ -1,6 +0,0 @@
fn increment(@x, by? = 1) {
return x + by
}
two = increment(1)
twentyOne = increment(1, by: 20)

View File

@ -1,177 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing kw_fn_with_defaults.kcl
snapshot_kind: text
---
{
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
},
"increment": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"end": 43,
"left": {
"end": 38,
"name": "x",
"start": 37,
"type": "Identifier",
"type": "Identifier"
},
"operator": "+",
"right": {
"end": 43,
"name": "by",
"start": 41,
"type": "Identifier",
"type": "Identifier"
},
"start": 37,
"type": "BinaryExpression",
"type": "BinaryExpression"
},
"end": 43,
"start": 30,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 45,
"start": 26
},
"end": 45,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 15,
"name": "x",
"start": 14,
"type": "Identifier"
},
"labeled": false
},
{
"type": "Parameter",
"identifier": {
"end": 19,
"name": "by",
"start": 17,
"type": "Identifier"
},
"default_value": {
"type": "Literal",
"type": "Literal",
"value": 1.0,
"raw": "1"
}
}
],
"start": 12,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
12,
45,
0
]
}
]
},
"twentyOne": {
"type": "Number",
"value": 21.0,
"__meta": [
{
"sourceRange": [
88,
89,
0
]
},
{
"sourceRange": [
95,
97,
0
]
}
]
},
"two": {
"type": "Number",
"value": 2.0,
"__meta": [
{
"sourceRange": [
63,
64,
0
]
}
]
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
}

View File

@ -87,6 +87,72 @@ snapshot_kind: text
} }
] ]
}, },
"arc_tag": {
"type": "TagIdentifier",
"type": "TagIdentifier",
"value": "arc_tag",
"info": {
"type": "TagEngineInfo",
"id": "[uuid]",
"sketch": "[uuid]",
"path": {
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
527,
549,
0
]
},
"ccw": true,
"center": [
200.0,
100.0
],
"from": [
280.0,
100.0
],
"radius": 80.0,
"tag": {
"end": 548,
"start": 540,
"type": "TagDeclarator",
"value": "arc_tag"
},
"to": [
280.0,
99.99999999999999
],
"type": "Arc"
},
"surface": {
"faceId": "[uuid]",
"id": "[uuid]",
"sourceRange": [
527,
549,
0
],
"tag": {
"end": 548,
"start": 540,
"type": "TagDeclarator",
"value": "arc_tag"
},
"type": "extrudeArc"
}
},
"__meta": [
{
"sourceRange": [
540,
548,
0
]
}
]
},
"b": { "b": {
"type": "TagIdentifier", "type": "TagIdentifier",
"type": "TagIdentifier", "type": "TagIdentifier",

View File

@ -1,586 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing tag_can_be_proxied_through_parameter.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 118,
"id": {
"end": 47,
"name": "myCircle",
"start": 39,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"argument": {
"arguments": [
{
"end": 106,
"properties": [
{
"end": 87,
"key": {
"end": 83,
"name": "radius",
"start": 77,
"type": "Identifier"
},
"start": 77,
"type": "ObjectProperty",
"value": {
"end": 87,
"raw": "4",
"start": 86,
"type": "Literal",
"type": "Literal",
"value": 4.0
}
},
{
"end": 104,
"key": {
"end": 95,
"name": "center",
"start": 89,
"type": "Identifier"
},
"start": 89,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 100,
"raw": "0",
"start": 99,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 103,
"raw": "0",
"start": 102,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 104,
"start": 98,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 75,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"end": 110,
"name": "sk",
"start": 108,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 115,
"name": "tag",
"start": 112,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 74,
"name": "circle",
"start": 68,
"type": "Identifier"
},
"end": 116,
"start": 68,
"type": "CallExpression",
"type": "CallExpression"
},
"end": 116,
"start": 61,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 118,
"start": 57
},
"end": 118,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 50,
"name": "sk",
"start": 48,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 55,
"name": "tag",
"start": 52,
"type": "Identifier"
}
}
],
"start": 47,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 39,
"type": "VariableDeclarator"
},
"end": 118,
"kind": "fn",
"start": 36,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 223,
"id": {
"end": 122,
"name": "c1",
"start": 120,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 143,
"raw": "'XY'",
"start": 139,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 138,
"name": "startSketchOn",
"start": 125,
"type": "Identifier"
},
"end": 144,
"start": 125,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 167,
"raw": "0",
"start": 166,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 170,
"raw": "0",
"start": 169,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 171,
"start": 165,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 174,
"start": 173,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 164,
"name": "startProfileAt",
"start": 150,
"type": "Identifier"
},
"end": 175,
"start": 150,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 191,
"start": 190,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 198,
"start": 193,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "mine"
}
],
"callee": {
"end": 189,
"name": "myCircle",
"start": 181,
"type": "Identifier"
},
"end": 199,
"start": 181,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 223,
"nonCodeMeta": {
"nonCodeNodes": {
"2": [
{
"end": 223,
"start": 199,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "The tag can be used.",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 125,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 120,
"type": "VariableDeclarator"
},
"end": 223,
"kind": "const",
"start": 120,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 257,
"id": {
"end": 228,
"name": "ang1",
"start": 224,
"type": "Identifier"
},
"init": {
"arguments": [
{
"computed": false,
"end": 256,
"object": {
"computed": false,
"end": 251,
"object": {
"end": 246,
"name": "c1",
"start": 244,
"type": "Identifier",
"type": "Identifier"
},
"property": {
"end": 251,
"name": "tags",
"start": 247,
"type": "Identifier",
"type": "Identifier"
},
"start": 244,
"type": "MemberExpression",
"type": "MemberExpression"
},
"property": {
"end": 256,
"name": "mine",
"start": 252,
"type": "Identifier",
"type": "Identifier"
},
"start": 244,
"type": "MemberExpression",
"type": "MemberExpression"
}
],
"callee": {
"end": 243,
"name": "tangentToEnd",
"start": 231,
"type": "Identifier"
},
"end": 257,
"start": 231,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 224,
"type": "VariableDeclarator"
},
"end": 257,
"kind": "const",
"start": 224,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 384,
"id": {
"end": 307,
"name": "c2",
"start": 305,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 328,
"raw": "'XY'",
"start": 324,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 323,
"name": "startSketchOn",
"start": 310,
"type": "Identifier"
},
"end": 329,
"start": 310,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 352,
"raw": "0",
"start": 351,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 355,
"raw": "0",
"start": 354,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 356,
"start": 350,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 359,
"start": 358,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 349,
"name": "startProfileAt",
"start": 335,
"type": "Identifier"
},
"end": 360,
"start": 335,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 376,
"start": 375,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 383,
"start": 378,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "mine"
}
],
"callee": {
"end": 374,
"name": "myCircle",
"start": 366,
"type": "Identifier"
},
"end": 384,
"start": 366,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 384,
"start": 310,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 305,
"type": "VariableDeclarator"
},
"end": 384,
"kind": "const",
"start": 305,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 418,
"id": {
"end": 389,
"name": "ang2",
"start": 385,
"type": "Identifier"
},
"init": {
"arguments": [
{
"computed": false,
"end": 417,
"object": {
"computed": false,
"end": 412,
"object": {
"end": 407,
"name": "c2",
"start": 405,
"type": "Identifier",
"type": "Identifier"
},
"property": {
"end": 412,
"name": "tags",
"start": 408,
"type": "Identifier",
"type": "Identifier"
},
"start": 405,
"type": "MemberExpression",
"type": "MemberExpression"
},
"property": {
"end": 417,
"name": "mine",
"start": 413,
"type": "Identifier",
"type": "Identifier"
},
"start": 405,
"type": "MemberExpression",
"type": "MemberExpression"
}
],
"callee": {
"end": 404,
"name": "tangentToEnd",
"start": 392,
"type": "Identifier"
},
"end": 418,
"start": 392,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 385,
"type": "VariableDeclarator"
},
"end": 418,
"kind": "const",
"start": 385,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 419,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 120,
"start": 118,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
],
"2": [
{
"end": 304,
"start": 257,
"type": "NonCodeNode",
"value": {
"type": "newLineBlockComment",
"value": "The same tag declarator can be used again.",
"style": "line"
}
}
]
},
"startNodes": [
{
"end": 35,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "A function with a tag parameter.",
"style": "line"
}
}
]
},
"start": 0
}
}

View File

@ -1,16 +0,0 @@
// A function with a tag parameter.
fn myCircle(sk, tag) {
return circle({ radius = 4, center = [0, 0] }, sk, tag)
}
c1 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> myCircle(%, $mine)
// The tag can be used.
ang1 = tangentToEnd(c1.tags.mine)
// The same tag declarator can be used again.
c2 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> myCircle(%, $mine)
ang2 = tangentToEnd(c2.tags.mine)

View File

@ -1,524 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Program memory after executing tag_can_be_proxied_through_parameter.kcl
snapshot_kind: text
---
{
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
},
"ang1": {
"type": "Number",
"value": 90.0,
"__meta": [
{
"sourceRange": [
231,
257,
0
]
}
]
},
"ang2": {
"type": "Number",
"value": 90.0,
"__meta": [
{
"sourceRange": [
392,
418,
0
]
}
]
},
"c1": {
"type": "Sketch",
"value": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 198,
"start": 193,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"__meta": []
},
"start": {
"from": [
4.0,
0.0
],
"to": [
4.0,
0.0
],
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
}
},
"tags": {
"mine": {
"type": "TagIdentifier",
"value": "mine",
"info": {
"type": "TagEngineInfo",
"id": "[uuid]",
"sketch": "[uuid]",
"path": {
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 198,
"start": 193,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
},
"surface": null
},
"__meta": [
{
"sourceRange": [
193,
198,
0
]
}
]
}
},
"__meta": [
{
"sourceRange": [
68,
116,
0
]
}
]
}
},
"c2": {
"type": "Sketch",
"value": {
"type": "Sketch",
"id": "[uuid]",
"paths": [
{
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 383,
"start": 378,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
}
],
"on": {
"type": "plane",
"id": "[uuid]",
"value": "XY",
"origin": {
"x": 0.0,
"y": 0.0,
"z": 0.0
},
"xAxis": {
"x": 1.0,
"y": 0.0,
"z": 0.0
},
"yAxis": {
"x": 0.0,
"y": 1.0,
"z": 0.0
},
"zAxis": {
"x": 0.0,
"y": 0.0,
"z": 1.0
},
"__meta": []
},
"start": {
"from": [
4.0,
0.0
],
"to": [
4.0,
0.0
],
"tag": null,
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
}
},
"tags": {
"mine": {
"type": "TagIdentifier",
"value": "mine",
"info": {
"type": "TagEngineInfo",
"id": "[uuid]",
"sketch": "[uuid]",
"path": {
"__geoMeta": {
"id": "[uuid]",
"sourceRange": [
68,
116,
0
]
},
"ccw": true,
"center": [
0.0,
0.0
],
"from": [
4.0,
0.0
],
"radius": 4.0,
"tag": {
"end": 383,
"start": 378,
"type": "TagDeclarator",
"value": "mine"
},
"to": [
4.0,
0.0
],
"type": "Circle"
},
"surface": null
},
"__meta": [
{
"sourceRange": [
378,
383,
0
]
}
]
}
},
"__meta": [
{
"sourceRange": [
68,
116,
0
]
}
]
}
},
"myCircle": {
"type": "Function",
"expression": {
"body": {
"body": [
{
"argument": {
"arguments": [
{
"end": 106,
"properties": [
{
"end": 87,
"key": {
"end": 83,
"name": "radius",
"start": 77,
"type": "Identifier"
},
"start": 77,
"type": "ObjectProperty",
"value": {
"end": 87,
"raw": "4",
"start": 86,
"type": "Literal",
"type": "Literal",
"value": 4.0
}
},
{
"end": 104,
"key": {
"end": 95,
"name": "center",
"start": 89,
"type": "Identifier"
},
"start": 89,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 100,
"raw": "0",
"start": 99,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 103,
"raw": "0",
"start": 102,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 104,
"start": 98,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 75,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"end": 110,
"name": "sk",
"start": 108,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 115,
"name": "tag",
"start": 112,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 74,
"name": "circle",
"start": 68,
"type": "Identifier"
},
"end": 116,
"start": 68,
"type": "CallExpression",
"type": "CallExpression"
},
"end": 116,
"start": 61,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 118,
"start": 57
},
"end": 118,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 50,
"name": "sk",
"start": 48,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 55,
"name": "tag",
"start": 52,
"type": "Identifier"
}
}
],
"start": 47,
"type": "FunctionExpression"
},
"memory": {
"environments": [
{
"bindings": {
"HALF_TURN": {
"type": "Number",
"value": 180.0,
"__meta": []
},
"QUARTER_TURN": {
"type": "Number",
"value": 90.0,
"__meta": []
},
"THREE_QUARTER_TURN": {
"type": "Number",
"value": 270.0,
"__meta": []
},
"ZERO": {
"type": "Number",
"value": 0.0,
"__meta": []
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
},
"__meta": [
{
"sourceRange": [
47,
118,
0
]
}
]
}
},
"parent": null
}
],
"currentEnv": 0,
"return": null
}

View File

@ -1,400 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Result of parsing tag_proxied_through_function_does_not_define_var.kcl
snapshot_kind: text
---
{
"Ok": {
"body": [
{
"declaration": {
"end": 193,
"id": {
"end": 47,
"name": "myCircle",
"start": 39,
"type": "Identifier"
},
"init": {
"body": {
"body": [
{
"declaration": {
"end": 113,
"id": {
"end": 62,
"name": "c",
"start": 61,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 103,
"properties": [
{
"end": 84,
"key": {
"end": 80,
"name": "radius",
"start": 74,
"type": "Identifier"
},
"start": 74,
"type": "ObjectProperty",
"value": {
"end": 84,
"raw": "4",
"start": 83,
"type": "Literal",
"type": "Literal",
"value": 4.0
}
},
{
"end": 101,
"key": {
"end": 92,
"name": "center",
"start": 86,
"type": "Identifier"
},
"start": 86,
"type": "ObjectProperty",
"value": {
"elements": [
{
"end": 97,
"raw": "0",
"start": 96,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 100,
"raw": "0",
"start": 99,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 101,
"start": 95,
"type": "ArrayExpression",
"type": "ArrayExpression"
}
}
],
"start": 72,
"type": "ObjectExpression",
"type": "ObjectExpression"
},
{
"end": 107,
"name": "sk",
"start": 105,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 112,
"name": "tag",
"start": 109,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 71,
"name": "circle",
"start": 65,
"type": "Identifier"
},
"end": 113,
"start": 65,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 61,
"type": "VariableDeclarator"
},
"end": 113,
"kind": "const",
"start": 61,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 173,
"id": {
"end": 152,
"name": "ang",
"start": 149,
"type": "Identifier"
},
"init": {
"arguments": [
{
"end": 172,
"name": "mine",
"start": 168,
"type": "Identifier",
"type": "Identifier"
}
],
"callee": {
"end": 167,
"name": "tangentToEnd",
"start": 155,
"type": "Identifier"
},
"end": 173,
"start": 155,
"type": "CallExpression",
"type": "CallExpression"
},
"start": 149,
"type": "VariableDeclarator"
},
"end": 173,
"kind": "const",
"start": 149,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"argument": {
"elements": [
{
"end": 185,
"name": "c",
"start": 184,
"type": "Identifier",
"type": "Identifier"
},
{
"end": 190,
"name": "ang",
"start": 187,
"type": "Identifier",
"type": "Identifier"
}
],
"end": 191,
"start": 183,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
"end": 191,
"start": 176,
"type": "ReturnStatement",
"type": "ReturnStatement"
}
],
"end": 193,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 146,
"start": 115,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "This should not be allowed.",
"style": "line"
}
}
]
},
"startNodes": []
},
"start": 57
},
"end": 193,
"params": [
{
"type": "Parameter",
"identifier": {
"end": 50,
"name": "sk",
"start": 48,
"type": "Identifier"
}
},
{
"type": "Parameter",
"identifier": {
"end": 55,
"name": "tag",
"start": 52,
"type": "Identifier"
}
}
],
"start": 47,
"type": "FunctionExpression",
"type": "FunctionExpression"
},
"start": 39,
"type": "VariableDeclarator"
},
"end": 193,
"kind": "fn",
"start": 36,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
},
{
"declaration": {
"end": 274,
"id": {
"end": 197,
"name": "c1",
"start": 195,
"type": "Identifier"
},
"init": {
"body": [
{
"arguments": [
{
"end": 218,
"raw": "'XY'",
"start": 214,
"type": "Literal",
"type": "Literal",
"value": "XY"
}
],
"callee": {
"end": 213,
"name": "startSketchOn",
"start": 200,
"type": "Identifier"
},
"end": 219,
"start": 200,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"elements": [
{
"end": 242,
"raw": "0",
"start": 241,
"type": "Literal",
"type": "Literal",
"value": 0.0
},
{
"end": 245,
"raw": "0",
"start": 244,
"type": "Literal",
"type": "Literal",
"value": 0.0
}
],
"end": 246,
"start": 240,
"type": "ArrayExpression",
"type": "ArrayExpression"
},
{
"end": 249,
"start": 248,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
}
],
"callee": {
"end": 239,
"name": "startProfileAt",
"start": 225,
"type": "Identifier"
},
"end": 250,
"start": 225,
"type": "CallExpression",
"type": "CallExpression"
},
{
"arguments": [
{
"end": 266,
"start": 265,
"type": "PipeSubstitution",
"type": "PipeSubstitution"
},
{
"end": 273,
"start": 268,
"type": "TagDeclarator",
"type": "TagDeclarator",
"value": "mine"
}
],
"callee": {
"end": 264,
"name": "myCircle",
"start": 256,
"type": "Identifier"
},
"end": 274,
"start": 256,
"type": "CallExpression",
"type": "CallExpression"
}
],
"end": 274,
"start": 200,
"type": "PipeExpression",
"type": "PipeExpression"
},
"start": 195,
"type": "VariableDeclarator"
},
"end": 274,
"kind": "const",
"start": 195,
"type": "VariableDeclaration",
"type": "VariableDeclaration"
}
],
"end": 275,
"nonCodeMeta": {
"nonCodeNodes": {
"0": [
{
"end": 195,
"start": 193,
"type": "NonCodeNode",
"value": {
"type": "newLine"
}
}
]
},
"startNodes": [
{
"end": 35,
"start": 0,
"type": "NonCodeNode",
"value": {
"type": "blockComment",
"value": "A function with a tag parameter.",
"style": "line"
}
}
]
},
"start": 0
}
}

View File

@ -1,20 +0,0 @@
---
source: kcl/src/simulation_tests.rs
description: Error from executing tag_proxied_through_function_does_not_define_var.kcl
snapshot_kind: text
---
KCL UndefinedValue error
× undefined value: memory item key `mine` is not defined
╭─[5:22]
4 │ // This should not be allowed.
5 │ ang = tangentToEnd(mine)
· ────
6 │ return [c, ang]
7 │ }
8 │
9 │ c1 = startSketchOn('XY')
10 │ |> startProfileAt([0, 0], %)
11 │ |> myCircle(%, $mine)
· ──────────────────
╰────

View File

@ -1,11 +0,0 @@
// A function with a tag parameter.
fn myCircle(sk, tag) {
c = circle({ radius = 4, center = [0, 0] }, sk, tag)
// This should not be allowed.
ang = tangentToEnd(mine)
return [c, ang]
}
c1 = startSketchOn('XY')
|> startProfileAt([0, 0], %)
|> myCircle(%, $mine)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 43 KiB